From e7cb92aa4ca3a3dfc33715e2641e7796417d835f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Catal=C3=A3o?= Date: Wed, 27 May 2026 13:17:33 +0100 Subject: [PATCH 1/3] feat: add per-channel audio capture with OPL/FM voice isolation Adds opt-in per-channel audio isolation for both PCM and OPL/FM synthesis paths, accessible via the "render.channel_capture" CTL. PCM isolation: CSoundFile::CreateStereoMix() snapshots MixSoundBuffer before and after each MixChannel() call, accumulating each channel's isolated contribution in m_channelCaptureBuf. OPL/FM isolation: Opal::Output() captures each voice's left-channel value before summation into m_voiceOut[]. OPL::Mix() reads these per-frame and stores them in m_voiceFrames[voice][frame], scaled to mix-buffer amplitude. New public API (C and C++): get_current_channel_audio_mono(channel, count, buf) get_current_channel_audio_stereo(channel, count, buf_left, buf_right) Both functions combine PCM and OPL contributions transparently, so callers do not need to know which synthesis path a channel is using. Data is valid after read() and before the next read() call. Usage: module.ctl_set_boolean("render.channel_capture", true); module.read_float_mono(samplerate, count, master_out); module.get_current_channel_audio_mono(ch, count, ch_out); --- libopenmpt/libopenmpt.h | 25 +++++++++++ libopenmpt/libopenmpt.hpp | 23 ++++++++++ libopenmpt/libopenmpt_c.cpp | 18 ++++++++ libopenmpt/libopenmpt_impl.cpp | 76 ++++++++++++++++++++++++++++++++++ libopenmpt/libopenmpt_impl.hpp | 3 ++ soundlib/Fastmix.cpp | 35 +++++++++++++++- soundlib/OPL.cpp | 16 +++++++ soundlib/OPL.h | 14 +++++++ soundlib/Sndfile.cpp | 16 +++++++ soundlib/Sndfile.h | 12 ++++++ soundlib/opal.h | 8 ++++ 11 files changed, 245 insertions(+), 1 deletion(-) diff --git a/libopenmpt/libopenmpt.h b/libopenmpt/libopenmpt.h index 486cada5b05..470699712c8 100644 --- a/libopenmpt/libopenmpt.h +++ b/libopenmpt/libopenmpt.h @@ -1275,6 +1275,31 @@ LIBOPENMPT_API float openmpt_module_get_current_channel_vu_rear_left( openmpt_mo */ LIBOPENMPT_API float openmpt_module_get_current_channel_vu_rear_right( openmpt_module * mod, int32_t channel ); +/*! \brief Get the per-channel audio output for the most recently rendered block, mixed to mono. + * + * \param mod The module handle to work on. + * \param channel The channel index (0-based). + * \param count Number of frames to retrieve. Must not exceed the count passed to the most recent openmpt_module_read_*() call. + * \param buf Output buffer of at least \p count floats. Values are in the range [-1.0, 1.0]. + * \return The number of frames written to \p buf, or 0 if capture is not enabled or the channel is out of range. + * \remarks Enable capture first via openmpt_module_ctl_set_boolean(mod, "render.channel_capture", 1). + * Works for both PCM and OPL/FM channels. Must be called after openmpt_module_read_*() and before the next read call. + */ +LIBOPENMPT_API size_t openmpt_module_get_current_channel_audio_mono( openmpt_module * mod, int32_t channel, size_t count, float * buf ); + +/*! \brief Get the per-channel audio output for the most recently rendered block, as separate left/right channels. + * + * \param mod The module handle to work on. + * \param channel The channel index (0-based). + * \param count Number of frames to retrieve. Must not exceed the count passed to the most recent openmpt_module_read_*() call. + * \param buf_left Output buffer of at least \p count floats for the left channel. + * \param buf_right Output buffer of at least \p count floats for the right channel. + * \return The number of frames written, or 0 if capture is not enabled or the channel is out of range. + * \remarks Enable capture first via openmpt_module_ctl_set_boolean(mod, "render.channel_capture", 1). + * Works for both PCM and OPL/FM channels. Must be called after openmpt_module_read_*() and before the next read call. + */ +LIBOPENMPT_API size_t openmpt_module_get_current_channel_audio_stereo( openmpt_module * mod, int32_t channel, size_t count, float * buf_left, float * buf_right ); + /*! \brief Get the number of sub-songs * * \param mod The module handle to work on. diff --git a/libopenmpt/libopenmpt.hpp b/libopenmpt/libopenmpt.hpp index 36e57bf0245..a492998c57e 100644 --- a/libopenmpt/libopenmpt.hpp +++ b/libopenmpt/libopenmpt.hpp @@ -950,6 +950,29 @@ class LIBOPENMPT_CXX_API_CLASS module { */ LIBOPENMPT_CXX_API_MEMBER float get_current_channel_vu_rear_right( std::int32_t channel ) const; + //! Get the per-channel audio output for the most recently rendered block, mixed to mono. + /*! + \param channel The channel index (0-based). + \param count Number of frames to retrieve. Must not exceed the count passed to the most recent read() call. + \param buf Output buffer of at least \p count floats. Values are in the range [-1.0, 1.0]. + \return The number of frames written to \p buf, or 0 if capture is not enabled or the channel is out of range. + \remarks Enable capture first via ctl_set_boolean("render.channel_capture", true). + Works for both PCM and OPL/FM channels. Must be called after read() and before the next read() call. + */ + LIBOPENMPT_CXX_API_MEMBER std::size_t get_current_channel_audio_mono( std::int32_t channel, std::size_t count, float * buf ) const; + + //! Get the per-channel audio output for the most recently rendered block, as separate left/right channels. + /*! + \param channel The channel index (0-based). + \param count Number of frames to retrieve. Must not exceed the count passed to the most recent read() call. + \param buf_left Output buffer of at least \p count floats for the left channel. + \param buf_right Output buffer of at least \p count floats for the right channel. + \return The number of frames written, or 0 if capture is not enabled or the channel is out of range. + \remarks Enable capture first via ctl_set_boolean("render.channel_capture", true). + Works for both PCM and OPL/FM channels. Must be called after read() and before the next read() call. + */ + LIBOPENMPT_CXX_API_MEMBER std::size_t get_current_channel_audio_stereo( std::int32_t channel, std::size_t count, float * buf_left, float * buf_right ) const; + //! Get the number of sub-songs /*! \return The number of sub-songs in the module. This includes any "hidden" songs (songs that share the same sequence, but start at different order indices) and "normal" sub-songs or "sequences" (if the format supports them). diff --git a/libopenmpt/libopenmpt_c.cpp b/libopenmpt/libopenmpt_c.cpp index e61bff27b78..68adde92247 100644 --- a/libopenmpt/libopenmpt_c.cpp +++ b/libopenmpt/libopenmpt_c.cpp @@ -1110,6 +1110,24 @@ float openmpt_module_get_current_channel_vu_rear_right( openmpt_module * mod, in } return 0.0; } +size_t openmpt_module_get_current_channel_audio_mono( openmpt_module * mod, int32_t channel, size_t count, float * buf ) { + try { + openmpt::interface::check_soundfile( mod ); + return mod->impl->get_current_channel_audio_mono( channel, count, buf ); + } catch ( ... ) { + openmpt::report_exception( __func__, mod ); + } + return 0; +} +size_t openmpt_module_get_current_channel_audio_stereo( openmpt_module * mod, int32_t channel, size_t count, float * buf_left, float * buf_right ) { + try { + openmpt::interface::check_soundfile( mod ); + return mod->impl->get_current_channel_audio_stereo( channel, count, buf_left, buf_right ); + } catch ( ... ) { + openmpt::report_exception( __func__, mod ); + } + return 0; +} int32_t openmpt_module_get_num_subsongs( openmpt_module * mod ) { try { diff --git a/libopenmpt/libopenmpt_impl.cpp b/libopenmpt/libopenmpt_impl.cpp index 4b826fca5c2..bd922dedca8 100644 --- a/libopenmpt/libopenmpt_impl.cpp +++ b/libopenmpt/libopenmpt_impl.cpp @@ -49,6 +49,7 @@ #include "common/FileReader.h" #include "common/Logging.h" #include "soundlib/Sndfile.h" +#include "soundlib/OPL.h" #include "soundlib/mod_specifications.h" #include "soundlib/AudioReadTarget.h" @@ -1407,6 +1408,75 @@ float module_impl::get_current_channel_vu_rear_right( std::int32_t channel ) con } return m_sndFile->m_PlayState.Chn[channel].dwFlags[OpenMPT::CHN_SURROUND] ? m_sndFile->m_PlayState.Chn[channel].nRightVU * (1.0f/128.0f) : 0.0f; } +std::size_t module_impl::get_current_channel_audio_mono( std::int32_t channel, std::size_t count, float * buf ) const { + if ( !buf || count == 0 || channel < 0 || channel >= m_sndFile->GetNumChannels() ) { + return 0; + } + if ( !m_sndFile->m_channelCaptureEnabled || m_sndFile->m_channelCaptureBuf.empty() ) { + return 0; + } + const OpenMPT::CHANNELINDEX ch = static_cast( channel ); + if ( ch >= m_sndFile->m_channelCaptureNumChannels ) { + return 0; + } + const std::size_t n = std::min( count, static_cast( MIXBUFFERSIZE ) ); + const float scale = m_sndFile->m_channelCaptureGlobalVolScale / OpenMPT::MIXING_SCALEF; + const OpenMPT::mixsample_t * src = m_sndFile->m_channelCaptureBuf.data() + static_cast( ch ) * MIXBUFFERSIZE * 2; + for ( std::size_t s = 0; s < n; ++s ) { + buf[s] = static_cast( src[s * 2] ) * scale; + } + // Add OPL voice contributions for this tracker channel + if ( m_sndFile->m_opl && m_sndFile->m_opl->IsVoiceCaptureEnabled() ) { + const OpenMPT::OPL * opl = m_sndFile->m_opl.get(); + const int nf = std::min( opl->GetCapturedFrameCount(), static_cast( n ) ); + constexpr float invMixScale = 1.0f / OpenMPT::MIXING_SCALEF; + const float oplScale = m_sndFile->m_channelCaptureGlobalVolScale * invMixScale; + for ( int v = 0; v < OpenMPT::OPL::NUM_OPL_VOICES; ++v ) { + if ( opl->GetVoiceTrackerChannel( v ) != ch ) continue; + const float * vf = opl->GetVoiceFrames( v ); + if ( !vf ) continue; + for ( int s = 0; s < nf; ++s ) + buf[s] += vf[s] * oplScale; + } + } + return n; +} +std::size_t module_impl::get_current_channel_audio_stereo( std::int32_t channel, std::size_t count, float * buf_left, float * buf_right ) const { + if ( !buf_left || !buf_right || count == 0 || channel < 0 || channel >= m_sndFile->GetNumChannels() ) { + return 0; + } + if ( !m_sndFile->m_channelCaptureEnabled || m_sndFile->m_channelCaptureBuf.empty() ) { + return 0; + } + const OpenMPT::CHANNELINDEX ch = static_cast( channel ); + if ( ch >= m_sndFile->m_channelCaptureNumChannels ) { + return 0; + } + const std::size_t n = std::min( count, static_cast( MIXBUFFERSIZE ) ); + const float scale = m_sndFile->m_channelCaptureGlobalVolScale / OpenMPT::MIXING_SCALEF; + const OpenMPT::mixsample_t * src = m_sndFile->m_channelCaptureBuf.data() + static_cast( ch ) * MIXBUFFERSIZE * 2; + for ( std::size_t s = 0; s < n; ++s ) { + buf_left[s] = static_cast( src[s * 2] ) * scale; + buf_right[s] = static_cast( src[s * 2 + 1] ) * scale; + } + // Add OPL voice contributions for this tracker channel + if ( m_sndFile->m_opl && m_sndFile->m_opl->IsVoiceCaptureEnabled() ) { + const OpenMPT::OPL * opl = m_sndFile->m_opl.get(); + const int nf = std::min( opl->GetCapturedFrameCount(), static_cast( n ) ); + constexpr float invMixScale = 1.0f / OpenMPT::MIXING_SCALEF; + const float oplScale = m_sndFile->m_channelCaptureGlobalVolScale * invMixScale; + for ( int v = 0; v < OpenMPT::OPL::NUM_OPL_VOICES; ++v ) { + if ( opl->GetVoiceTrackerChannel( v ) != ch ) continue; + const float * vf = opl->GetVoiceFrames( v ); + if ( !vf ) continue; + for ( int s = 0; s < nf; ++s ) { + buf_left[s] += vf[s] * oplScale; + buf_right[s] += vf[s] * oplScale; + } + } + } + return n; +} std::int32_t module_impl::get_num_subsongs() const { std::unique_ptr subsongs_temp = has_subsongs_inited() ? std::unique_ptr() : std::make_unique( get_subsongs() ); @@ -1734,6 +1804,7 @@ std::pair module_i { "render.resampler.emulate_amiga", ctl_type::boolean }, { "render.resampler.emulate_amiga_type", ctl_type::text }, { "render.opl.volume_factor", ctl_type::floatingpoint }, + { "render.channel_capture", ctl_type::boolean }, { "dither", ctl_type::integer } }; return std::make_pair(std::begin(ctl_infos), std::end(ctl_infos)); @@ -1831,6 +1902,8 @@ bool module_impl::ctl_get_boolean( std::string_view ctl, bool throw_if_unknown ) return m_ctl_seek_sync_samples; } else if ( ctl == "render.resampler.emulate_amiga" ) { return ( m_sndFile->m_Resampler.m_Settings.emulateAmiga != OpenMPT::Resampling::AmigaFilter::Off ); + } else if ( ctl == "render.channel_capture" ) { + return m_ctl_render_channel_capture; } else { MPT_ASSERT_NOTREACHED(); return false; @@ -2062,6 +2135,9 @@ void module_impl::ctl_set_boolean( std::string_view ctl, bool value, bool throw_ if ( newsettings != m_sndFile->m_Resampler.m_Settings ) { m_sndFile->SetResamplerSettings( newsettings ); } + } else if ( ctl == "render.channel_capture" ) { + m_ctl_render_channel_capture = value; + m_sndFile->SetChannelCaptureEnabled( value, m_sndFile->GetNumChannels() ); } else { MPT_ASSERT_NOTREACHED(); } diff --git a/libopenmpt/libopenmpt_impl.hpp b/libopenmpt/libopenmpt_impl.hpp index 7e5faac4b04..5bc76b728d2 100644 --- a/libopenmpt/libopenmpt_impl.hpp +++ b/libopenmpt/libopenmpt_impl.hpp @@ -150,6 +150,7 @@ class module_impl { bool m_ctl_load_skip_plugins; bool m_ctl_load_skip_subsongs_init; bool m_ctl_seek_sync_samples; + bool m_ctl_render_channel_capture = false; std::vector m_loaderMessages; public: void PushToCSoundFileLog( const std::string & text ) const; @@ -239,6 +240,8 @@ class module_impl { float get_current_channel_vu_right( std::int32_t channel ) const; float get_current_channel_vu_rear_left( std::int32_t channel ) const; float get_current_channel_vu_rear_right( std::int32_t channel ) const; + std::size_t get_current_channel_audio_mono( std::int32_t channel, std::size_t count, float * buf ) const; + std::size_t get_current_channel_audio_stereo( std::int32_t channel, std::size_t count, float * buf_left, float * buf_right ) const; std::int32_t get_num_subsongs() const; std::int32_t get_num_channels() const; std::int32_t get_num_orders() const; diff --git a/soundlib/Fastmix.cpp b/soundlib/Fastmix.cpp index fe3c3abe17e..54d8cb1a6b0 100644 --- a/soundlib/Fastmix.cpp +++ b/soundlib/Fastmix.cpp @@ -309,13 +309,46 @@ void CSoundFile::CreateStereoMix(int count) if(m_MixerSettings.gnChannels > 2) StereoFill(MixRearBuffer, count, m_surroundROfsVol, m_surroundLOfsVol); + if(m_channelCaptureEnabled && !m_channelCaptureBuf.empty()) + { + std::fill(m_channelCaptureBuf.begin(), m_channelCaptureBuf.end(), mixsample_t{}); + m_channelCaptureGlobalVolScale = m_PlayConfig.getGlobalVolumeAppliesToMaster() + ? static_cast(m_PlayState.m_nGlobalVolume) / static_cast(MAX_GLOBAL_VOLUME) + : 1.0f; + } + // Channels that are actually mixed and not skipped (because they are paused or muted) CHANNELINDEX numChannelsMixed = 0; for(uint32 nChn = 0; nChn < m_nMixChannels; nChn++) { - if(MixChannel(count, m_PlayState.Chn[m_PlayState.ChnMix[nChn]], m_PlayState.ChnMix[nChn], numChannelsMixed < m_MixerSettings.m_nMaxMixChannels)) + const CHANNELINDEX absIdx = m_PlayState.ChnMix[nChn]; + + int captureTarget = -1; + mixsample_t snapBuf[MIXBUFFERSIZE * 2]; + if(m_channelCaptureEnabled && !m_channelCaptureBuf.empty()) + { + if(absIdx < m_channelCaptureNumChannels) + captureTarget = static_cast(absIdx); + else + { + const CHANNELINDEX master = m_PlayState.Chn[absIdx].nMasterChn; + if(master > 0 && master <= m_channelCaptureNumChannels) + captureTarget = static_cast(master - 1); + } + if(captureTarget >= 0) + std::memcpy(snapBuf, MixSoundBuffer, count * 2 * sizeof(mixsample_t)); + } + + if(MixChannel(count, m_PlayState.Chn[absIdx], absIdx, numChannelsMixed < m_MixerSettings.m_nMaxMixChannels)) numChannelsMixed++; + + if(m_channelCaptureEnabled && captureTarget >= 0) + { + mixsample_t *dest = m_channelCaptureBuf.data() + static_cast(captureTarget) * MIXBUFFERSIZE * 2; + for(int s = 0; s < count * 2; ++s) + dest[s] += MixSoundBuffer[s] - snapBuf[s]; + } } m_nMixStat = std::max(m_nMixStat, numChannelsMixed); } diff --git a/soundlib/OPL.cpp b/soundlib/OPL.cpp index afc305a6504..73bc6f57a99 100644 --- a/soundlib/OPL.cpp +++ b/soundlib/OPL.cpp @@ -60,6 +60,8 @@ void OPL::Mix(int32 *target, size_t count, uint32 volumeFactorQ16) // This factor causes a sample voice to be more or less as loud as an OPL voice const int32 factor = Util::muldiv_unsigned(volumeFactorQ16, 6169, (1 << 16)); + if(m_captureEnabled) + m_captureFrameCount = 0; while(count--) { int16 l, r; @@ -67,10 +69,24 @@ void OPL::Mix(int32 *target, size_t count, uint32 volumeFactorQ16) target[0] += l * factor; target[1] += r * factor; target += 2; + if(m_captureEnabled && m_captureFrameCount < MAX_CAPTURE_FRAMES) + { + const int16_t *voiceOut = m_opl->GetVoiceOutput(); + for(int v = 0; v < OPL_CHANNELS; ++v) + m_voiceFrames[v][m_captureFrameCount] = static_cast(voiceOut[v]) * static_cast(factor); + ++m_captureFrameCount; + } } } +void OPL::SetupVoiceCapture(bool enable) noexcept +{ + m_captureEnabled = enable; + m_captureFrameCount = 0; +} + + OPL::Register OPL::ChannelToRegister(uint8 oplCh) { if(oplCh < 9) diff --git a/soundlib/OPL.h b/soundlib/OPL.h index 214a0060285..71a3ba03fba 100644 --- a/soundlib/OPL.h +++ b/soundlib/OPL.h @@ -87,6 +87,16 @@ class OPL void Initialize(uint32 sampleRate); void Mix(int32 *buffer, size_t count, uint32 volumeFactorQ16); + // Per-voice audio capture — opt-in, enabled via "render.channel_capture" CTL. + // Must be called before Mix(); data is valid until the next Mix() call. + void SetupVoiceCapture(bool enable) noexcept; + bool IsVoiceCaptureEnabled() const noexcept { return m_captureEnabled; } + int GetCapturedFrameCount() const noexcept { return m_captureFrameCount; } + const float* GetVoiceFrames(int voice) const noexcept { return m_captureEnabled ? m_voiceFrames[voice] : nullptr; } + CHANNELINDEX GetVoiceTrackerChannel(int voice) const noexcept { return m_OPLtoChan[voice]; } + static constexpr int MAX_CAPTURE_FRAMES = 512; + static constexpr int NUM_OPL_VOICES = 18; + void NoteOff(CHANNELINDEX c); void NoteCut(CHANNELINDEX c, bool unassign = true); void Frequency(CHANNELINDEX c, uint32 milliHertz, bool keyOff, bool beatingOscillators); @@ -130,6 +140,10 @@ class OPL std::array m_Patches; bool m_isActive = false; + + bool m_captureEnabled{false}; + int m_captureFrameCount{0}; + float m_voiceFrames[OPL_CHANNELS][MAX_CAPTURE_FRAMES]{}; }; OPENMPT_NAMESPACE_END diff --git a/soundlib/Sndfile.cpp b/soundlib/Sndfile.cpp index 272f760df6a..78b2806987b 100644 --- a/soundlib/Sndfile.cpp +++ b/soundlib/Sndfile.cpp @@ -2192,4 +2192,20 @@ void TempoSwing::Deserialize(std::istream &iStrm, TempoSwing &swing, const size_ } +void CSoundFile::SetChannelCaptureEnabled(bool enable, CHANNELINDEX numChannels) +{ + m_channelCaptureEnabled = enable; + m_channelCaptureNumChannels = enable ? numChannels : 0; + if(enable) + m_channelCaptureBuf.assign(static_cast(numChannels) * MIXBUFFERSIZE * 2, mixsample_t{}); + else + { + m_channelCaptureBuf.clear(); + m_channelCaptureBuf.shrink_to_fit(); + } + if(m_opl) + m_opl->SetupVoiceCapture(enable); +} + + OPENMPT_NAMESPACE_END diff --git a/soundlib/Sndfile.h b/soundlib/Sndfile.h index f083f7f4b6d..861b76428f3 100644 --- a/soundlib/Sndfile.h +++ b/soundlib/Sndfile.h @@ -437,6 +437,18 @@ class CSoundFile // Interleaved Front Mix Buffer (Also room for interleaved rear mix) mixsample_t MixSoundBuffer[MIXBUFFERSIZE * 4]; mixsample_t MixRearBuffer[MIXBUFFERSIZE * 2]; + +public: + // Per-channel audio capture — enabled via "render.channel_capture" CTL. + // Allocated on enable, freed on disable. Interleaved stereo: [ch * MIXBUFFERSIZE * 2]. + bool m_channelCaptureEnabled{false}; + CHANNELINDEX m_channelCaptureNumChannels{0}; + float m_channelCaptureGlobalVolScale{1.0f}; + std::vector m_channelCaptureBuf; + + void SetChannelCaptureEnabled(bool enable, CHANNELINDEX numChannels); + +private: // Non-interleaved plugin processing buffer float MixFloatBuffer[2][MIXBUFFERSIZE]; mixsample_t MixInputBuffer[NUMMIXINPUTBUFFERS][MIXBUFFERSIZE]; diff --git a/soundlib/opal.h b/soundlib/opal.h index b10ad285d49..2ee4a5b5bfd 100644 --- a/soundlib/opal.h +++ b/soundlib/opal.h @@ -179,6 +179,12 @@ class Opal { void Port(uint16_t reg_num, uint8_t val); void Sample(int16_t *left, int16_t *right); + public: + // Per-voice output snapshot — valid after each Sample() call. + // Returns a pointer to NumChannels int16 values: the isolated left-channel + // contribution of each OPL voice before summation in Output(). + const int16_t * GetVoiceOutput() const noexcept { return m_voiceOut; } + protected: void Init(int sample_rate); void Output(int16_t &left, int16_t &right); @@ -186,6 +192,7 @@ class Opal { int32_t SampleRate; int32_t SampleAccum; int16_t LastOutput[2], CurrOutput[2]; + int16_t m_voiceOut[NumChannels]{}; Channel Chan[NumChannels]; Operator Op[NumOperators]; // uint16_t ExpTable[256]; @@ -596,6 +603,7 @@ void Opal::Output(int16_t &left, int16_t &right) { int16_t chanleft, chanright; Chan[i].Output(chanleft, chanright); + m_voiceOut[i] = chanleft; leftmix += chanleft; rightmix += chanright; From ae52b4d27a08819153488c940e453ec55f84af9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Catal=C3=A3o?= Date: Wed, 27 May 2026 14:43:28 +0100 Subject: [PATCH 2/3] feat: add mono/stereo channel audio retrieval methods and improve voice output fallback logic --- libopenmpt/libopenmpt_cxx.cpp | 7 +++++++ soundlib/opal.h | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/libopenmpt/libopenmpt_cxx.cpp b/libopenmpt/libopenmpt_cxx.cpp index d85b8ac0f74..ea84c638622 100644 --- a/libopenmpt/libopenmpt_cxx.cpp +++ b/libopenmpt/libopenmpt_cxx.cpp @@ -362,6 +362,13 @@ float module::get_current_channel_vu_rear_right( std::int32_t channel ) const { return impl->get_current_channel_vu_rear_right( channel ); } +std::size_t module::get_current_channel_audio_mono( std::int32_t channel, std::size_t count, float * buf ) const { + return impl->get_current_channel_audio_mono( channel, count, buf ); +} +std::size_t module::get_current_channel_audio_stereo( std::int32_t channel, std::size_t count, float * buf_left, float * buf_right ) const { + return impl->get_current_channel_audio_stereo( channel, count, buf_left, buf_right ); +} + std::int32_t module::get_num_subsongs() const { return impl->get_num_subsongs(); } diff --git a/soundlib/opal.h b/soundlib/opal.h index 2ee4a5b5bfd..659b7cd97be 100644 --- a/soundlib/opal.h +++ b/soundlib/opal.h @@ -603,7 +603,7 @@ void Opal::Output(int16_t &left, int16_t &right) { int16_t chanleft, chanright; Chan[i].Output(chanleft, chanright); - m_voiceOut[i] = chanleft; + m_voiceOut[i] = chanleft != 0 ? chanleft : chanright; leftmix += chanleft; rightmix += chanright; From a30d0049cbf936fc20a9910c0fc467772bf9fdb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Catal=C3=A3o?= Date: Thu, 18 Jun 2026 16:59:34 +0100 Subject: [PATCH 3/3] Clarify comment on channel capture buffer allocation --- soundlib/Sndfile.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soundlib/Sndfile.h b/soundlib/Sndfile.h index 861b76428f3..d2c263059d0 100644 --- a/soundlib/Sndfile.h +++ b/soundlib/Sndfile.h @@ -440,7 +440,7 @@ class CSoundFile public: // Per-channel audio capture — enabled via "render.channel_capture" CTL. - // Allocated on enable, freed on disable. Interleaved stereo: [ch * MIXBUFFERSIZE * 2]. + // Allocated on enable, freed on disable - Interleaved stereo: [ch * MIXBUFFERSIZE * 2]. bool m_channelCaptureEnabled{false}; CHANNELINDEX m_channelCaptureNumChannels{0}; float m_channelCaptureGlobalVolScale{1.0f};