View Issue Details

IDProjectCategoryView StatusLast Update
0001080OpenMPTlibopenmptpublic2022-01-14 18:15
ReporterSaga Musix Assigned To 
PrioritynormalSeverityfeatureReproducibilityN/A
Status newResolutionopen 
Target VersionOpenMPT 1.?? (libopenmpt 1.0) (goals) 
Summary0001080: Auto-normalize
Description

Add a ctl to libopenmpt that allows to auto-normalize module playback, like BASS/XMPlay.
As far as I'm aware, XMPlay renders the module at a very low mix rate (1000 Hz or so) for its length detection and normalization procedure. We could do something similar, or add the logic to GetLength, which might be less accurate but faster.

TagsNo tags attached.
Attached Files
Has the bug occurred in previous versions?
Tested code revision (in case you know it)

Activities

Saga Musix

Saga Musix

2018-01-25 19:35

administrator   ~0003396

Proof-of-concept patch added. It works surprisingly well already, but there are some things to consider. For example, we might want to offer different quality levels for the auto-normalization. Right now, it works at 1000 Hz / mono, but doing it in stereo at a higher sample rate can be more accurate. Dropping plugins from the scan process can also increase speed, but make the result less accurate.

auto-normalize.patch (5,596 bytes)   
Index: libopenmpt/libopenmpt_impl.cpp
===================================================================
--- libopenmpt/libopenmpt_impl.cpp	(revision 9521)
+++ libopenmpt/libopenmpt_impl.cpp	(working copy)
@@ -450,6 +450,7 @@
 	m_current_subsong = 0;
 	m_currentPositionSeconds = 0.0;
 	m_Gain = 1.0f;
+	m_AutoGain = 1.0f;
 	m_ctl_play_at_end = song_end_action::fadeout_song;
 	m_ctl_load_skip_samples = false;
 	m_ctl_load_skip_patterns = false;
@@ -493,6 +494,7 @@
 	for ( const auto & ctl : ctls ) {
 		ctl_set( ctl.first, ctl.second, false );
 	}
+	update_autogain();
 }
 bool module_impl::is_loaded() const {
 	return m_loaded;
@@ -503,7 +505,7 @@
 	std::size_t count_read = 0;
 	while ( count > 0 ) {
 		std::int16_t * const buffers[4] = { left + count_read, right + count_read, rear_left + count_read, rear_right + count_read };
-		AudioReadTargetGainBuffer<std::int16_t> target(*m_Dither, 0, buffers, m_Gain);
+		AudioReadTargetGainBuffer<std::int16_t> target(*m_Dither, 0, buffers, m_Gain * m_AutoGain);
 		std::size_t count_chunk = m_sndFile->Read(
 			static_cast<CSoundFile::samplecount_t>( std::min<std::uint64_t>( count, std::numeric_limits<CSoundFile::samplecount_t>::max() / 2 / 4 / 4 ) ), // safety margin / samplesize / channels
 			target
@@ -526,7 +528,7 @@
 	std::size_t count_read = 0;
 	while ( count > 0 ) {
 		float * const buffers[4] = { left + count_read, right + count_read, rear_left + count_read, rear_right + count_read };
-		AudioReadTargetGainBuffer<float> target(*m_Dither, 0, buffers, m_Gain);
+		AudioReadTargetGainBuffer<float> target(*m_Dither, 0, buffers, m_Gain * m_AutoGain);
 		std::size_t count_chunk = m_sndFile->Read(
 			static_cast<CSoundFile::samplecount_t>( std::min<std::uint64_t>( count, std::numeric_limits<CSoundFile::samplecount_t>::max() / 2 / 4 / 4 ) ), // safety margin / samplesize / channels
 			target
@@ -548,7 +550,7 @@
 	m_sndFile->m_bIsRendering = ( m_ctl_play_at_end != song_end_action::fadeout_song );
 	std::size_t count_read = 0;
 	while ( count > 0 ) {
-		AudioReadTargetGainBuffer<std::int16_t> target(*m_Dither, interleaved + count_read * channels, 0, m_Gain);
+		AudioReadTargetGainBuffer<std::int16_t> target(*m_Dither, interleaved + count_read * channels, 0, m_Gain * m_AutoGain);
 		std::size_t count_chunk = m_sndFile->Read(
 			static_cast<CSoundFile::samplecount_t>( std::min<std::uint64_t>( count, std::numeric_limits<CSoundFile::samplecount_t>::max() / 2 / 4 / 4 ) ), // safety margin / samplesize / channels
 			target
@@ -570,7 +572,7 @@
 	m_sndFile->m_bIsRendering = ( m_ctl_play_at_end != song_end_action::fadeout_song );
 	std::size_t count_read = 0;
 	while ( count > 0 ) {
-		AudioReadTargetGainBuffer<float> target(*m_Dither, interleaved + count_read * channels, 0, m_Gain);
+		AudioReadTargetGainBuffer<float> target(*m_Dither, interleaved + count_read * channels, 0, m_Gain * m_AutoGain);
 		std::size_t count_chunk = m_sndFile->Read(
 			static_cast<CSoundFile::samplecount_t>( std::min<std::uint64_t>( count, std::numeric_limits<CSoundFile::samplecount_t>::max() / 2 / 4 / 4 ) ), // safety margin / samplesize / channels
 			target
@@ -1658,4 +1660,42 @@
 	}
 }
 
+void module_impl::update_autogain() {
+	std::int32_t current_subsong = get_selected_subsong();
+	select_subsong( all_subsongs);
+
+	std::uint64_t samples = Util::Round<std::uint64_t>( get_duration_seconds() * 1000.0 );
+	float buffer[512];
+	auto old_dither = m_Dither->GetMode();
+	m_Dither->SetMode( DitherNone );
+	auto orig_settings = m_sndFile->m_Resampler.m_Settings, new_settings = m_sndFile->m_Resampler.m_Settings;
+	new_settings.emulateAmiga = false;
+	new_settings.SrcMode = SRCMODE_NEAREST;
+	m_sndFile->SetResamplerSettings( new_settings );
+
+	apply_mixer_settings( 1000, 1 );
+	float peak = 0.0f;
+	while ( samples > 0 ) {
+		AudioReadTargetBuffer<float> target(*m_Dither, buffer, nullptr );
+		std::size_t count_chunk = m_sndFile->Read(
+			static_cast<std::uint32_t>( std::min<std::uint64_t>( mpt::size(buffer), samples ) ),
+			target
+		);
+		if(count_chunk == 0) {
+			break;
+		}
+		auto minmax = std::minmax_element( std::begin( buffer ), std::begin( buffer ) + count_chunk );
+		peak = std::max( { peak, mpt::abs(*minmax.first), mpt::abs(*minmax.second) } );
+		samples -= count_chunk;
+	}
+
+	m_Dither->SetMode( old_dither );
+	m_sndFile->SetResamplerSettings( orig_settings );
+	select_subsong( current_subsong );
+
+	if ( peak > 0.0f ) {
+		m_AutoGain = 1.0f / peak;
+	}
+}
+
 } // namespace openmpt
Index: libopenmpt/libopenmpt_impl.hpp
===================================================================
--- libopenmpt/libopenmpt_impl.hpp	(revision 9521)
+++ libopenmpt/libopenmpt_impl.hpp	(working copy)
@@ -98,7 +98,7 @@
 	bool m_loaded;
 	std::unique_ptr<OpenMPT::Dither> m_Dither;
 	subsongs_type m_subsongs;
-	float m_Gain;
+	float m_Gain, m_AutoGain;
 	song_end_action m_ctl_play_at_end;
 	bool m_ctl_load_skip_samples;
 	bool m_ctl_load_skip_patterns;
@@ -126,6 +126,7 @@
 	std::pair< std::string, std::string > format_and_highlight_pattern_row_channel_command( std::int32_t p, std::int32_t r, std::int32_t c, int command ) const;
 	std::pair< std::string, std::string > format_and_highlight_pattern_row_channel( std::int32_t p, std::int32_t r, std::int32_t c, std::size_t width, bool pad ) const;
 	static double could_open_probability( const OpenMPT::FileReader & file, double effort, std::unique_ptr<log_interface> log );
+	void update_autogain();
 public:
 	static std::vector<std::string> get_supported_extensions();
 	static bool is_extension_supported( const char * extension );
auto-normalize.patch (5,596 bytes)   
Saga Musix

Saga Musix

2022-01-14 18:15

administrator   ~0004997

auto-normalize-2.patch (6,810 bytes)   
Index: libopenmpt/libopenmpt_impl.cpp
===================================================================
--- libopenmpt/libopenmpt_impl.cpp	(revision 16515)
+++ libopenmpt/libopenmpt_impl.cpp	(working copy)
@@ -468,6 +468,7 @@
 	m_current_subsong = 0;
 	m_currentPositionSeconds = 0.0;
 	m_Gain = 1.0f;
+	m_AutoGain = 1.0f;
 	m_ctl_play_at_end = song_end_action::fadeout_song;
 	m_ctl_load_skip_samples = false;
 	m_ctl_load_skip_patterns = false;
@@ -511,6 +512,7 @@
 	for ( const auto & ctl : ctls ) {
 		ctl_set( ctl.first, ctl.second, false );
 	}
+	update_autogain();
 }
 bool module_impl::is_loaded() const {
 	return m_loaded;
@@ -520,7 +522,7 @@
 	m_sndFile->m_bIsRendering = ( m_ctl_play_at_end != song_end_action::fadeout_song );
 	std::size_t count_read = 0;
 	std::int16_t * const buffers[4] = { left, right, rear_left, rear_right };
-	OpenMPT::AudioTargetBufferWithGain<mpt::audio_span_planar<std::int16_t>> target( mpt::audio_span_planar<std::int16_t>( buffers, valid_channels( buffers, std::size( buffers ) ), count ), *m_Dithers, m_Gain );
+	OpenMPT::AudioTargetBufferWithGain<mpt::audio_span_planar<std::int16_t>> target( mpt::audio_span_planar<std::int16_t>( buffers, valid_channels( buffers, std::size( buffers ) ), count ), *m_Dithers, m_Gain * m_AutoGain );
 	while ( count > 0 ) {
 		std::size_t count_chunk = m_sndFile->Read(
 			static_cast<OpenMPT::CSoundFile::samplecount_t>( std::min( static_cast<std::uint64_t>( count ), static_cast<std::uint64_t>( std::numeric_limits<OpenMPT::CSoundFile::samplecount_t>::max() / 2 / 4 / 4 ) ) ), // safety margin / samplesize / channels
@@ -543,7 +545,7 @@
 	m_sndFile->m_bIsRendering = ( m_ctl_play_at_end != song_end_action::fadeout_song );
 	std::size_t count_read = 0;
 	float * const buffers[4] = { left, right, rear_left, rear_right };
-	OpenMPT::AudioTargetBufferWithGain<mpt::audio_span_planar<float>> target( mpt::audio_span_planar<float>( buffers, valid_channels( buffers, std::size( buffers ) ), count ), *m_Dithers, m_Gain );
+	OpenMPT::AudioTargetBufferWithGain<mpt::audio_span_planar<float>> target( mpt::audio_span_planar<float>( buffers, valid_channels( buffers, std::size( buffers ) ), count ), *m_Dithers, m_Gain * m_AutoGain );
 	while ( count > 0 ) {
 		std::size_t count_chunk = m_sndFile->Read(
 			static_cast<OpenMPT::CSoundFile::samplecount_t>( std::min( static_cast<std::uint64_t>( count ), static_cast<std::uint64_t>( std::numeric_limits<OpenMPT::CSoundFile::samplecount_t>::max() / 2 / 4 / 4 ) ) ), // safety margin / samplesize / channels
@@ -565,7 +567,7 @@
 	m_sndFile->ResetMixStat();
 	m_sndFile->m_bIsRendering = ( m_ctl_play_at_end != song_end_action::fadeout_song );
 	std::size_t count_read = 0;
-	OpenMPT::AudioTargetBufferWithGain<mpt::audio_span_interleaved<std::int16_t>> target( mpt::audio_span_interleaved<std::int16_t>( interleaved, channels, count ), *m_Dithers, m_Gain );
+	OpenMPT::AudioTargetBufferWithGain<mpt::audio_span_interleaved<std::int16_t>> target( mpt::audio_span_interleaved<std::int16_t>( interleaved, channels, count ), *m_Dithers, m_Gain * m_AutoGain );
 	while ( count > 0 ) {
 		std::size_t count_chunk = m_sndFile->Read(
 			static_cast<OpenMPT::CSoundFile::samplecount_t>( std::min( static_cast<std::uint64_t>( count ), static_cast<std::uint64_t>( std::numeric_limits<OpenMPT::CSoundFile::samplecount_t>::max() / 2 / 4 / 4 ) ) ), // safety margin / samplesize / channels
@@ -587,7 +589,7 @@
 	m_sndFile->ResetMixStat();
 	m_sndFile->m_bIsRendering = ( m_ctl_play_at_end != song_end_action::fadeout_song );
 	std::size_t count_read = 0;
-	OpenMPT::AudioTargetBufferWithGain<mpt::audio_span_interleaved<float>> target( mpt::audio_span_interleaved<float>( interleaved, channels, count ), *m_Dithers, m_Gain );
+	OpenMPT::AudioTargetBufferWithGain<mpt::audio_span_interleaved<float>> target( mpt::audio_span_interleaved<float>( interleaved, channels, count ), *m_Dithers, m_Gain * m_AutoGain );
 	while ( count > 0 ) {
 		std::size_t count_chunk = m_sndFile->Read(
 			static_cast<OpenMPT::CSoundFile::samplecount_t>( std::min( static_cast<std::uint64_t>( count ), static_cast<std::uint64_t>( std::numeric_limits<OpenMPT::CSoundFile::samplecount_t>::max() / 2 / 4 / 4 ) ) ), // safety margin / samplesize / channels
@@ -2141,4 +2143,42 @@
 	}
 }
 
+void module_impl::update_autogain() {
+	std::int32_t current_subsong = get_selected_subsong();
+	select_subsong( all_subsongs);
+
+	std::uint64_t samples = Util::Round<std::uint64_t>( get_duration_seconds() * 1000.0 );
+	float buffer[512];
+	auto old_dither = m_Dither->GetMode();
+	m_Dither->SetMode( DitherNone );
+	auto orig_settings = m_sndFile->m_Resampler.m_Settings, new_settings = m_sndFile->m_Resampler.m_Settings;
+	new_settings.emulateAmiga = false;
+	new_settings.SrcMode = SRCMODE_NEAREST;
+	m_sndFile->SetResamplerSettings( new_settings );
+
+	apply_mixer_settings( 1000, 1 );
+	float peak = 0.0f;
+	while ( samples > 0 ) {
+		AudioReadTargetBuffer<float> target(*m_Dither, buffer, nullptr );
+		std::size_t count_chunk = m_sndFile->Read(
+			static_cast<std::uint32_t>( std::min<std::uint64_t>( mpt::size(buffer), samples ) ),
+			target
+		);
+		if(count_chunk == 0) {
+			break;
+		}
+		auto minmax = std::minmax_element( std::begin( buffer ), std::begin( buffer ) + count_chunk );
+		peak = std::max( { peak, mpt::abs(*minmax.first), mpt::abs(*minmax.second) } );
+		samples -= count_chunk;
+	}
+
+	m_Dither->SetMode( old_dither );
+	m_sndFile->SetResamplerSettings( orig_settings );
+	select_subsong( current_subsong );
+
+	if ( peak > 0.0f ) {
+		m_AutoGain = 1.0f / peak;
+	}
+}
+
 } // namespace openmpt
Index: libopenmpt/libopenmpt_impl.hpp
===================================================================
--- libopenmpt/libopenmpt_impl.hpp	(revision 16515)
+++ libopenmpt/libopenmpt_impl.hpp	(working copy)
@@ -131,7 +131,7 @@
 	bool m_mixer_initialized;
 	std::unique_ptr<OpenMPT::DithersWrapperOpenMPT> m_Dithers;
 	subsongs_type m_subsongs;
-	float m_Gain;
+	float m_Gain, m_AutoGain;
 	song_end_action m_ctl_play_at_end;
 	amiga_filter_type m_ctl_render_resampler_emulate_amiga_type = amiga_filter_type::auto_filter;
 	bool m_ctl_load_skip_samples;
@@ -162,6 +162,7 @@
 	std::pair< std::string, std::string > format_and_highlight_pattern_row_channel_command( std::int32_t p, std::int32_t r, std::int32_t c, int command ) const;
 	std::pair< std::string, std::string > format_and_highlight_pattern_row_channel( std::int32_t p, std::int32_t r, std::int32_t c, std::size_t width, bool pad ) const;
 	static double could_open_probability( const OpenMPT::FileCursor & file, double effort, std::unique_ptr<log_interface> log );
+	void update_autogain();
 public:
 	static std::vector<std::string> get_supported_extensions();
 	static bool is_extension_supported( std::string_view extension );
auto-normalize-2.patch (6,810 bytes)   

Issue History

Date Modified Username Field Change
2018-01-25 14:54 Saga Musix New Issue
2018-01-25 14:55 manx Target Version => OpenMPT 1.?? (libopenmpt 1.0) (goals)
2018-01-25 19:35 Saga Musix File Added: auto-normalize.patch
2018-01-25 19:35 Saga Musix Note Added: 0003396
2022-01-14 18:15 Saga Musix Note Added: 0004997
2022-01-14 18:15 Saga Musix File Added: auto-normalize-2.patch