Index: common/mptString.cpp
===================================================================
--- common/mptString.cpp	(revision 12255)
+++ common/mptString.cpp	(working copy)
@@ -1772,7 +1772,7 @@
 	return 0;
 }
 
-int CompareNoCaseAscii(const std::string &a, const std::string &b)
+int CompareNoCaseAscii(std::string_view a, std::string_view b)
 {
 	for(std::size_t i = 0; i < std::min(a.length(), b.length()); ++i)
 	{
@@ -1793,6 +1793,12 @@
 	return a.length() < b.length() ? -1 : 1;
 }
 
+int CompareNoCaseAscii(const std::string &a, const std::string &b)
+{
+	return CompareNoCaseAscii(std::string_view(a), std::string_view(b));
+}
+
+
 #if defined(MODPLUG_TRACKER)
 
 mpt::ustring ToLowerCase(const mpt::ustring &s)
Index: common/mptString.h
===================================================================
--- common/mptString.h	(revision 12255)
+++ common/mptString.h	(working copy)
@@ -19,6 +19,7 @@
 #include <algorithm>
 #include <limits>
 #include <string>
+#include <string_view>
 
 #include <cstring>
 
@@ -524,8 +525,10 @@
 std::string ToUpperCaseAscii(std::string s);
 
 int CompareNoCaseAscii(const char *a, const char *b, std::size_t n);
+int CompareNoCaseAscii(std::string_view a, std::string_view b);
 int CompareNoCaseAscii(const std::string &a, const std::string &b);
 
+
 #if defined(MODPLUG_TRACKER)
 
 mpt::ustring ToLowerCase(const mpt::ustring &s);
Index: common/stdafx.h
===================================================================
--- common/stdafx.h	(revision 12255)
+++ common/stdafx.h	(working copy)
@@ -115,6 +115,7 @@
 // <algorithm>
 // <limits>
 // <string>
+// <string_view>
 // <type_traits>
 // <cstring>
 
Index: libopenmpt/libopenmpt.h
===================================================================
--- libopenmpt/libopenmpt.h	(revision 12255)
+++ libopenmpt/libopenmpt.h	(working copy)
@@ -1396,21 +1396,21 @@
  * \param mod The module handle to work on.
  * \return A semicolon-separated list containing all supported ctl keys.
  * \remarks Currently supported ctl values are:
- *          - load.skip_samples: Set to "1" to avoid loading samples into memory
- *          - load.skip_patterns: Set to "1" to avoid loading patterns into memory
- *          - load.skip_plugins: Set to "1" to avoid loading plugins
- *          - load.skip_subsongs_init: Set to "1" to avoid pre-initializing sub-songs. Skipping results in faster module loading but slower seeking.
- *          - seek.sync_samples: Set to "1" to sync sample playback when using openmpt_module_set_position_seconds or openmpt_module_set_position_order_row.
- *          - subsong: The current subsong. Setting it has identical semantics as openmpt_module_select_subsong(), getting it returns the currently selected subsong.
- *          - play.at_end: Chooses the behaviour when the end of song is reached:
+ *          - load.skip_samples (boolean): Set to "1" to avoid loading samples into memory
+ *          - load.skip_patterns (boolean): Set to "1" to avoid loading patterns into memory
+ *          - load.skip_plugins (boolean): Set to "1" to avoid loading plugins
+ *          - load.skip_subsongs_init (boolean): Set to "1" to avoid pre-initializing sub-songs. Skipping results in faster module loading but slower seeking.
+ *          - seek.sync_samples (boolean): Set to "1" to sync sample playback when using openmpt_module_set_position_seconds or openmpt_module_set_position_order_row.
+ *          - subsong (integer): The current subsong. Setting it has identical semantics as openmpt_module_select_subsong(), getting it returns the currently selected subsong.
+ *          - play.at_end (text): Chooses the behaviour when the end of song is reached:
  *                         - "fadeout": Fades the module out for a short while. Subsequent reads after the fadeout will return 0 rendered frames.
  *                         - "continue": Returns 0 rendered frames when the song end is reached. Subsequent reads will continue playing from the song start or loop start.
  *                         - "stop": Returns 0 rendered frames when the song end is reached. Subsequent reads will return 0 rendered frames.
- *          - play.tempo_factor: Set a floating point tempo factor. "1.0" is the default tempo.
- *          - play.pitch_factor: Set a floating point pitch factor. "1.0" is the default pitch.
- *          - render.resampler.emulate_amiga: Set to "1" to enable the Amiga resampler for Amiga modules. This emulates the sound characteristics of the Paula chip and overrides the selected interpolation filter. Non-Amiga module formats are not affected by this setting.
- *          - render.opl.volume_factor: Set volume factor applied to synthesized OPL sounds, relative to the default OPL volume.
- *          - dither: Set the dither algorithm that is used for the 16 bit versions of openmpt_module_read. Supported values are:
+ *          - play.tempo_factor (floatingpoint): Set a floating point tempo factor. "1.0" is the default tempo.
+ *          - play.pitch_factor (floatingpoint): Set a floating point pitch factor. "1.0" is the default pitch.
+ *          - render.resampler.emulate_amiga (boolean): Set to "1" to enable the Amiga resampler for Amiga modules. This emulates the sound characteristics of the Paula chip and overrides the selected interpolation filter. Non-Amiga module formats are not affected by this setting.
+ *          - render.opl.volume_factor (floatingpoint): Set volume factor applied to synthesized OPL sounds, relative to the default OPL volume.
+ *          - dither (integer): Set the dither algorithm that is used for the 16 bit versions of openmpt_module_read. Supported values are:
  *                    - 0: No dithering.
  *                    - 1: Default mode. Chosen by OpenMPT code, might change.
  *                    - 2: Rectangular, 0.5 bit depth, no noise shaping (original ModPlug Tracker).
@@ -1417,6 +1417,7 @@
  *                    - 3: Rectangular, 1 bit depth, simple 1st order noise shaping
  */
 LIBOPENMPT_API const char * openmpt_module_get_ctls( openmpt_module * mod );
+
 /*! \brief Get current ctl value
  *
  * \param mod The module handle to work on.
@@ -1423,8 +1424,46 @@
  * \param ctl The ctl key whose value should be retrieved.
  * \return The associated ctl value, or NULL on failure.
  * \sa openmpt_module_get_ctls
+ * \deprecated Please use openmpt_module_ctl_get_boolean(), openmpt_module_ctl_get_integer(), openmpt_module_ctl_get_floatingpoint(), or openmpt_module_ctl_get_text().
  */
-LIBOPENMPT_API const char * openmpt_module_ctl_get( openmpt_module * mod, const char * ctl );
+LIBOPENMPT_API LIBOPENMPT_DEPRECATED const char * openmpt_module_ctl_get( openmpt_module * mod, const char * ctl );
+/*! \brief Get current ctl boolean value
+ *
+ * \param mod The module handle to work on.
+ * \param ctl The ctl key whose value should be retrieved.
+ * \return The associated ctl value, or NULL on failure.
+ * \sa openmpt_module_get_ctls
+ * \since 0.5.0
+ */
+LIBOPENMPT_API int openmpt_module_ctl_get_boolean( openmpt_module * mod, const char * ctl );
+/*! \brief Get current ctl integer value
+ *
+ * \param mod The module handle to work on.
+ * \param ctl The ctl key whose value should be retrieved.
+ * \return The associated ctl value, or NULL on failure.
+ * \sa openmpt_module_get_ctls
+ * \since 0.5.0
+ */
+LIBOPENMPT_API int64_t openmpt_module_ctl_get_integer( openmpt_module * mod, const char * ctl );
+/*! \brief Get current ctl floatingpoint value
+ *
+ * \param mod The module handle to work on.
+ * \param ctl The ctl key whose value should be retrieved.
+ * \return The associated ctl value, or NULL on failure.
+ * \sa openmpt_module_get_ctls
+ * \since 0.5.0
+ */
+LIBOPENMPT_API double openmpt_module_ctl_get_floatingpoint( openmpt_module * mod, const char * ctl );
+/*! \brief Get current ctl string value
+ *
+ * \param mod The module handle to work on.
+ * \param ctl The ctl key whose value should be retrieved.
+ * \return The associated ctl value, or NULL on failure.
+ * \sa openmpt_module_get_ctls
+ * \since 0.5.0
+ */
+LIBOPENMPT_API const char * openmpt_module_ctl_get_text( openmpt_module * mod, const char * ctl );
+
 /*! \brief Set ctl value
  *
  * \param mod The module handle to work on.
@@ -1432,8 +1471,49 @@
  * \param value The value that should be set.
  * \return 1 if successful, 0 in case the value is not sensible (e.g. negative tempo factor) or the ctl is not recognized.
  * \sa openmpt_module_get_ctls
+ * \deprecated Please use openmpt_module_ctl_set_boolean(), openmpt_module_ctl_set_integer(), openmpt_module_ctl_set_floatingpoint(), or openmpt_module_ctl_set_text().
  */
-LIBOPENMPT_API int openmpt_module_ctl_set( openmpt_module * mod, const char * ctl, const char * value );
+LIBOPENMPT_API LIBOPENMPT_DEPRECATED int openmpt_module_ctl_set( openmpt_module * mod, const char * ctl, const char * value );
+/*! \brief Set ctl boolean value
+ *
+ * \param mod The module handle to work on.
+ * \param ctl The ctl key whose value should be set.
+ * \param value The value that should be set.
+ * \return 1 if successful, 0 in case the value is not sensible (e.g. negative tempo factor) or the ctl is not recognized.
+ * \sa openmpt_module_get_ctls
+ * \since 0.5.0
+ */
+LIBOPENMPT_API int openmpt_module_ctl_set_boolean( openmpt_module * mod, const char * ctl, int value );
+/*! \brief Set ctl integer value
+ *
+ * \param mod The module handle to work on.
+ * \param ctl The ctl key whose value should be set.
+ * \param value The value that should be set.
+ * \return 1 if successful, 0 in case the value is not sensible (e.g. negative tempo factor) or the ctl is not recognized.
+ * \sa openmpt_module_get_ctls
+ * \since 0.5.0
+ */
+LIBOPENMPT_API int openmpt_module_ctl_set_integer( openmpt_module * mod, const char * ctl, int64_t value );
+/*! \brief Set ctl floatingpoint value
+ *
+ * \param mod The module handle to work on.
+ * \param ctl The ctl key whose value should be set.
+ * \param value The value that should be set.
+ * \return 1 if successful, 0 in case the value is not sensible (e.g. negative tempo factor) or the ctl is not recognized.
+ * \sa openmpt_module_get_ctls
+ * \since 0.5.0
+ */
+LIBOPENMPT_API int openmpt_module_ctl_set_floatingpoint( openmpt_module * mod, const char * ctl, double value );
+/*! \brief Set ctl string value
+ *
+ * \param mod The module handle to work on.
+ * \param ctl The ctl key whose value should be set.
+ * \param value The value that should be set.
+ * \return 1 if successful, 0 in case the value is not sensible (e.g. negative tempo factor) or the ctl is not recognized.
+ * \sa openmpt_module_get_ctls
+ * \since 0.5.0
+ */
+LIBOPENMPT_API int openmpt_module_ctl_set_text( openmpt_module * mod, const char * ctl, const char * value );
 
 /* remember to add new functions to both C and C++ interfaces and to increase OPENMPT_API_VERSION_MINOR */
 
Index: libopenmpt/libopenmpt.hpp
===================================================================
--- libopenmpt/libopenmpt.hpp	(revision 12255)
+++ libopenmpt/libopenmpt.hpp	(working copy)
@@ -17,6 +17,7 @@
 #include <iostream>
 #include <map>
 #include <string>
+#include <string_view>
 #include <vector>
 
 #include <cstdint>
@@ -231,8 +232,16 @@
 /*!
   \param extension file extension to query without a leading dot. The case is ignored.
   \return true if the extension is supported by libopenmpt, false otherwise.
+  \deprecated Please use openmpt::is_extension_supported2().
 */
-LIBOPENMPT_CXX_API bool is_extension_supported( const std::string & extension );
+LIBOPENMPT_ATTR_DEPRECATED LIBOPENMPT_CXX_API bool is_extension_supported( const std::string & extension );
+//! Query whether a file extension is supported
+/*!
+  \param extension file extension to query without a leading dot. The case is ignored.
+  \return true if the extension is supported by libopenmpt, false otherwise.
+  \since 0.5.0
+*/
+LIBOPENMPT_CXX_API bool is_extension_supported2( std::string_view extension );
 
 //! Roughly scan the input stream to find out whether libopenmpt might be able to open it
 /*!
@@ -962,21 +971,21 @@
 	/*!
 	  \return A vector containing all supported ctl keys.
 	  \remarks Currently supported ctl values are:
-	           - load.skip_samples: Set to "1" to avoid loading samples into memory
-	           - load.skip_patterns: Set to "1" to avoid loading patterns into memory
-	           - load.skip_plugins: Set to "1" to avoid loading plugins
-	           - load.skip_subsongs_init: Set to "1" to avoid pre-initializing sub-songs. Skipping results in faster module loading but slower seeking.
-	           - seek.sync_samples: Set to "1" to sync sample playback when using openmpt::module::set_position_seconds or openmpt::module::set_position_order_row.
-	           - subsong: The current subsong. Setting it has identical semantics as openmpt::module::select_subsong(), getting it returns the currently selected subsong.
-	           - play.at_end: Chooses the behaviour when the end of song is reached:
+	           - load.skip_samples (boolean): Set to "1" to avoid loading samples into memory
+	           - load.skip_patterns (boolean): Set to "1" to avoid loading patterns into memory
+	           - load.skip_plugins (boolean): Set to "1" to avoid loading plugins
+	           - load.skip_subsongs_init (boolean): Set to "1" to avoid pre-initializing sub-songs. Skipping results in faster module loading but slower seeking.
+	           - seek.sync_samples (boolean): Set to "1" to sync sample playback when using openmpt::module::set_position_seconds or openmpt::module::set_position_order_row.
+	           - subsong (integer): The current subsong. Setting it has identical semantics as openmpt::module::select_subsong(), getting it returns the currently selected subsong.
+	           - play.at_end (text): Chooses the behaviour when the end of song is reached:
 	                          - "fadeout": Fades the module out for a short while. Subsequent reads after the fadeout will return 0 rendered frames.
 	                          - "continue": Returns 0 rendered frames when the song end is reached. Subsequent reads will continue playing from the song start or loop start.
 	                          - "stop": Returns 0 rendered frames when the song end is reached. Subsequent reads will return 0 rendered frames.
-	           - play.tempo_factor: Set a floating point tempo factor. "1.0" is the default tempo.
-	           - play.pitch_factor: Set a floating point pitch factor. "1.0" is the default pitch.
-	           - render.resampler.emulate_amiga: Set to "1" to enable the Amiga resampler for Amiga modules. This emulates the sound characteristics of the Paula chip and overrides the selected interpolation filter. Non-Amiga module formats are not affected by this setting. 
-	           - render.opl.volume_factor: Set volume factor applied to synthesized OPL sounds, relative to the default OPL volume.
-	           - dither: Set the dither algorithm that is used for the 16 bit versions of openmpt::module::read. Supported values are:
+	           - play.tempo_factor (floatingpoint): Set a floating point tempo factor. "1.0" is the default tempo.
+	           - play.pitch_factor (floatingpoint): Set a floating point pitch factor. "1.0" is the default pitch.
+	           - render.resampler.emulate_amiga (boolean): Set to "1" to enable the Amiga resampler for Amiga modules. This emulates the sound characteristics of the Paula chip and overrides the selected interpolation filter. Non-Amiga module formats are not affected by this setting. 
+	           - render.opl.volume_factor (floatingpoint): Set volume factor applied to synthesized OPL sounds, relative to the default OPL volume.
+	           - dither (integer): Set the dither algorithm that is used for the 16 bit versions of openmpt::module::read. Supported values are:
 	                     - 0: No dithering.
 	                     - 1: Default mode. Chosen by OpenMPT code, might change.
 	                     - 2: Rectangular, 0.5 bit depth, no noise shaping (original ModPlug Tracker).
@@ -991,8 +1000,42 @@
 	  \param ctl The ctl key whose value should be retrieved.
 	  \return The associated ctl value.
 	  \sa openmpt::module::get_ctls
+	  \deprecated Please use openmpt::module::ctl_get_boolean(), openmpt::module::ctl_get_integer(), openmpt::module::ctl_get_floatingpoint(), or openmpt::module::ctl_get_text().
 	*/
-	std::string ctl_get( const std::string & ctl ) const;
+	LIBOPENMPT_ATTR_DEPRECATED std::string ctl_get( const std::string & ctl ) const;
+	//! Get current ctl boolean value
+	/*!
+	  \param ctl The ctl key whose value should be retrieved.
+	  \return The associated ctl value.
+	  \sa openmpt::module::get_ctls
+	  \since 0.5.0
+	*/
+	bool ctl_get_boolean( std::string_view ctl ) const;
+	//! Get current ctl integer value
+	/*!
+	  \param ctl The ctl key whose value should be retrieved.
+	  \return The associated ctl value.
+	  \sa openmpt::module::get_ctls
+	  \since 0.5.0
+	*/
+	std::int64_t ctl_get_integer( std::string_view ctl ) const;
+	//! Get current ctl floatingpoint value
+	/*!
+	  \param ctl The ctl key whose value should be retrieved.
+	  \return The associated ctl value.
+	  \sa openmpt::module::get_ctls
+	  \since 0.5.0
+	*/
+	double ctl_get_floatingpoint( std::string_view ctl ) const;
+	//! Get current ctl text value
+	/*!
+	  \param ctl The ctl key whose value should be retrieved.
+	  \return The associated ctl value.
+	  \sa openmpt::module::get_ctls
+	  \since 0.5.0
+	*/
+	std::string ctl_get_text( std::string_view ctl ) const;
+
 	//! Set ctl value
 	/*!
 	  \param ctl The ctl key whose value should be set.
@@ -999,8 +1042,45 @@
 	  \param value The value that should be set.
 	  \throws openmpt::exception Throws an exception derived from openmpt::exception in case the value is not sensible (e.g. negative tempo factor) or under the circumstances outlined in openmpt::module::get_ctls.
 	  \sa openmpt::module::get_ctls
+	  \deprecated Please use openmpt::module::ctl_set_bool(), openmpt::module::ctl_set_int(), openmpt::module::ctl_set_float(), or openmpt::module::ctl_set_string().
 	*/
-	void ctl_set( const std::string & ctl, const std::string & value );
+	LIBOPENMPT_ATTR_DEPRECATED void ctl_set( const std::string & ctl, const std::string & value );
+	//! Set ctl boolean value
+	/*!
+	  \param ctl The ctl key whose value should be set.
+	  \param value The value that should be set.
+	  \throws openmpt::exception Throws an exception derived from openmpt::exception in case the value is not sensible (e.g. negative tempo factor) or under the circumstances outlined in openmpt::module::get_ctls.
+	  \sa openmpt::module::get_ctls
+	  \since 0.5.0
+	*/
+	void ctl_set_boolean( std::string_view ctl, bool value );
+	//! Set ctl integer value
+	/*!
+	  \param ctl The ctl key whose value should be set.
+	  \param value The value that should be set.
+	  \throws openmpt::exception Throws an exception derived from openmpt::exception in case the value is not sensible (e.g. negative tempo factor) or under the circumstances outlined in openmpt::module::get_ctls.
+	  \sa openmpt::module::get_ctls
+	  \since 0.5.0
+	*/
+	void ctl_set_integer( std::string_view ctl, std::int64_t value );
+	//! Set ctl floatingpoint value
+	/*!
+	  \param ctl The ctl key whose value should be set.
+	  \param value The value that should be set.
+	  \throws openmpt::exception Throws an exception derived from openmpt::exception in case the value is not sensible (e.g. negative tempo factor) or under the circumstances outlined in openmpt::module::get_ctls.
+	  \sa openmpt::module::get_ctls
+	  \since 0.5.0
+	*/
+	void ctl_set_floatingpoint( std::string_view ctl, double value );
+	//! Set ctl text value
+	/*!
+	  \param ctl The ctl key whose value should be set.
+	  \param value The value that should be set.
+	  \throws openmpt::exception Throws an exception derived from openmpt::exception in case the value is not sensible (e.g. negative tempo factor) or under the circumstances outlined in openmpt::module::get_ctls.
+	  \sa openmpt::module::get_ctls
+	  \since 0.5.0
+	*/
+	void ctl_set_text( std::string_view ctl, std::string_view value );
 
 	// remember to add new functions to both C and C++ interfaces and to increase OPENMPT_API_VERSION_MINOR
 
Index: libopenmpt/libopenmpt_c.cpp
===================================================================
--- libopenmpt/libopenmpt_c.cpp	(revision 12255)
+++ libopenmpt/libopenmpt_c.cpp	(working copy)
@@ -1278,6 +1278,46 @@
 	}
 	return NULL;
 }
+int openmpt_module_ctl_get_boolean( openmpt_module * mod, const char * ctl ) {
+	try {
+		openmpt::interface::check_soundfile( mod );
+		openmpt::interface::check_pointer( ctl );
+		return mod->impl->ctl_get_boolean( ctl );
+	} catch ( ... ) {
+		openmpt::report_exception( __func__, mod );
+	}
+	return 0;
+}
+int64_t openmpt_module_ctl_get_integer( openmpt_module * mod, const char * ctl ) {
+	try {
+		openmpt::interface::check_soundfile( mod );
+		openmpt::interface::check_pointer( ctl );
+		return mod->impl->ctl_get_integer( ctl );
+	} catch ( ... ) {
+		openmpt::report_exception( __func__, mod );
+	}
+	return 0;
+}
+double openmpt_module_ctl_get_floatingpoint( openmpt_module * mod, const char * ctl ) {
+	try {
+		openmpt::interface::check_soundfile( mod );
+		openmpt::interface::check_pointer( ctl );
+		return mod->impl->ctl_get_floatingpoint( ctl );
+	} catch ( ... ) {
+		openmpt::report_exception( __func__, mod );
+	}
+	return 0.0;
+}
+const char * openmpt_module_ctl_get_text( openmpt_module * mod, const char * ctl ) {
+	try {
+		openmpt::interface::check_soundfile( mod );
+		openmpt::interface::check_pointer( ctl );
+		return openmpt::strdup( mod->impl->ctl_get_text( ctl ).c_str() );
+	} catch ( ... ) {
+		openmpt::report_exception( __func__, mod );
+	}
+	return NULL;
+}
 
 int openmpt_module_ctl_set( openmpt_module * mod, const char * ctl, const char * value ) {
 	try {
@@ -1291,8 +1331,52 @@
 	}
 	return 0;
 }
+int openmpt_module_ctl_set_boolean( openmpt_module * mod, const char * ctl, int value ) {
+	try {
+		openmpt::interface::check_soundfile( mod );
+		openmpt::interface::check_pointer( ctl );
+		mod->impl->ctl_set_boolean( ctl, value );
+		return 1;
+	} catch ( ... ) {
+		openmpt::report_exception( __func__, mod );
+	}
+	return 0;
+}
+int openmpt_module_ctl_set_integer( openmpt_module * mod, const char * ctl, int64_t value ) {
+	try {
+		openmpt::interface::check_soundfile( mod );
+		openmpt::interface::check_pointer( ctl );
+		mod->impl->ctl_set_integer( ctl, value );
+		return 1;
+	} catch ( ... ) {
+		openmpt::report_exception( __func__, mod );
+	}
+	return 0;
+}
+int openmpt_module_ctl_set_floatingpoint( openmpt_module * mod, const char * ctl, double value ) {
+	try {
+		openmpt::interface::check_soundfile( mod );
+		openmpt::interface::check_pointer( ctl );
+		mod->impl->ctl_set_floatingpoint( ctl, value );
+		return 1;
+	} catch ( ... ) {
+		openmpt::report_exception( __func__, mod );
+	}
+	return 0;
+}
+int openmpt_module_ctl_set_text( openmpt_module * mod, const char * ctl, const char * value ) {
+	try {
+		openmpt::interface::check_soundfile( mod );
+		openmpt::interface::check_pointer( ctl );
+		openmpt::interface::check_pointer( value );
+		mod->impl->ctl_set_text( ctl, value );
+		return 1;
+	} catch ( ... ) {
+		openmpt::report_exception( __func__, mod );
+	}
+	return 0;
+}
 
-
 openmpt_module_ext * openmpt_module_ext_create( openmpt_stream_callbacks stream_callbacks, void * stream, openmpt_log_func logfunc, void * loguser, openmpt_error_func errfunc, void * erruser, int * error, const char * * error_message, const openmpt_module_initial_ctl * ctls ) {
 	try {
 		openmpt_module_ext * mod_ext = (openmpt_module_ext*)std::calloc( 1, sizeof( openmpt_module_ext ) );
Index: libopenmpt/libopenmpt_cxx.cpp
===================================================================
--- libopenmpt/libopenmpt_cxx.cpp	(revision 12255)
+++ libopenmpt/libopenmpt_cxx.cpp	(working copy)
@@ -126,6 +126,9 @@
 bool is_extension_supported( const std::string & extension ) {
 	return openmpt::module_impl::is_extension_supported( extension );
 }
+bool is_extension_supported( std::string_view extension ) {
+	return openmpt::module_impl::is_extension_supported( extension );
+}
 
 double could_open_probability( std::istream & stream, double effort, std::ostream & log ) {
 	return openmpt::module_impl::could_open_probability( stream, effort, openmpt::helper::make_unique<std_ostream_log>( log ) );
@@ -385,12 +388,38 @@
 std::vector<std::string> module::get_ctls() const {
 	return impl->get_ctls();
 }
+
 std::string module::ctl_get( const std::string & ctl ) const {
 	return impl->ctl_get( ctl );
 }
+bool module::ctl_get_boolean( std::string_view ctl ) const {
+	return impl->ctl_get_boolean( ctl );
+}
+std::int64_t module::ctl_get_integer( std::string_view ctl ) const {
+	return impl->ctl_get_integer( ctl );
+}
+double module::ctl_get_floatingpoint( std::string_view ctl ) const {
+	return impl->ctl_get_floatingpoint( ctl );
+}
+std::string module::ctl_get_text( std::string_view ctl ) const {
+	return impl->ctl_get_text( ctl );
+}
+
 void module::ctl_set( const std::string & ctl, const std::string & value ) {
 	impl->ctl_set( ctl, value );
 }
+void module::ctl_set_boolean( std::string_view ctl, bool value ) {
+	impl->ctl_set_boolean( ctl, value );
+}
+void module::ctl_set_integer( std::string_view ctl, std::int64_t value ) {
+	impl->ctl_set_integer( ctl, value );
+}
+void module::ctl_set_floatingpoint( std::string_view ctl, double value ) {
+	impl->ctl_set_floatingpoint( ctl, value );
+}
+void module::ctl_set_text( std::string_view ctl, std::string_view value ) {
+	impl->ctl_set_text( ctl, value );
+}
 
 module_ext::module_ext( std::istream & stream, std::ostream & log, const std::map< std::string, std::string > & ctls ) : ext_impl(0) {
 	ext_impl = new module_ext_impl( stream, openmpt::helper::make_unique<std_ostream_log>( log ), ctls );
Index: libopenmpt/libopenmpt_impl.cpp
===================================================================
--- libopenmpt/libopenmpt_impl.cpp	(revision 12255)
+++ libopenmpt/libopenmpt_impl.cpp	(working copy)
@@ -593,12 +593,9 @@
 	std::copy( extensions.begin(), extensions.end(), std::back_insert_iterator<std::vector<std::string> >( retval ) );
 	return retval;
 }
-bool module_impl::is_extension_supported( const char * extension ) {
+bool module_impl::is_extension_supported( std::string_view extension ) {
 	return CSoundFile::IsExtensionSupported( extension );
 }
-bool module_impl::is_extension_supported( const std::string & extension ) {
-	return CSoundFile::IsExtensionSupported( extension.c_str() );
-}
 double module_impl::could_open_probability( const OpenMPT::FileReader & file, double effort, std::unique_ptr<log_interface> log ) {
 	try {
 		if ( effort >= 0.8 ) {
@@ -1510,23 +1507,34 @@
 	return format_and_highlight_pattern_row_channel( p, r, c, width, pad ).second;
 }
 
-std::vector<std::string> module_impl::get_ctls() const {
-	return
-	{
-		"load.skip_samples",
-		"load.skip_patterns",
-		"load.skip_plugins",
-		"load.skip_subsongs_init",
-		"seek.sync_samples",
-		"subsong",
-		"play.tempo_factor",
-		"play.pitch_factor",
-		"play.at_end",
-		"render.resampler.emulate_amiga",
-		"render.opl.volume_factor",
-		"dither",
+std::pair<const module_impl::ctl_info *, const module_impl::ctl_info *> module_impl::get_ctl_infos() const {
+	static constexpr ctl_info ctl_infos[] = {
+		{ "load.skip_samples", ctl_type::boolean },
+		{ "load.skip_patterns", ctl_type::boolean },
+		{ "load.skip_plugins", ctl_type::boolean },
+		{ "load.skip_subsongs_init", ctl_type::boolean },
+		{ "seek.sync_samples", ctl_type::boolean },
+		{ "subsong", ctl_type::integer },
+		{ "play.tempo_factor", ctl_type::floatingpoint },
+		{ "play.pitch_factor", ctl_type::floatingpoint },
+		{ "play.at_end", ctl_type::text },
+		{ "render.resampler.emulate_amiga", ctl_type::boolean },
+		{ "render.opl.volume_factor", ctl_type::floatingpoint },
+		{ "dither", ctl_type::integer }
 	};
+	return std::make_pair(std::begin(ctl_infos), std::end(ctl_infos));
 }
+
+std::vector<std::string> module_impl::get_ctls() const {
+	std::vector<std::string> result;
+	auto ctl_infos = get_ctl_infos();
+	result.reserve(std::distance(ctl_infos.first, ctl_infos.second));
+	for ( std::ptrdiff_t i = 0; i < std::distance(ctl_infos.first, ctl_infos.second); ++i ) {
+		result.push_back(ctl_infos.first[i].name);
+	}
+	return result;
+}
+
 std::string module_impl::ctl_get( std::string ctl, bool throw_if_unknown ) const {
 	if ( !ctl.empty() ) {
 		// cppcheck false-positive
@@ -1541,20 +1549,189 @@
 			ctl = ctl.substr( 0, ctl.length() - 1 );
 		}
 	}
+	auto found_ctl = std::find_if(get_ctl_infos().first, get_ctl_infos().second, [&](const ctl_info & info) -> bool { return info.name == ctl; });
+	if ( found_ctl == get_ctl_infos().second ) {
+		if ( ctl == "" ) {
+			throw openmpt::exception("empty ctl");
+		} else if ( throw_if_unknown ) {
+			throw openmpt::exception("unknown ctl: " + ctl);
+		} else {
+			return std::string();
+		}
+	}
+	std::string result;
+	switch ( found_ctl->type ) {
+		case ctl_type::boolean:
+			return mpt::fmt::val( ctl_get_boolean( ctl, throw_if_unknown ) );
+			break;
+		case ctl_type::integer:
+			return mpt::fmt::val( ctl_get_integer( ctl, throw_if_unknown ) );
+			break;
+		case ctl_type::floatingpoint:
+			return mpt::fmt::val( ctl_get_floatingpoint( ctl, throw_if_unknown ) );
+			break;
+		case ctl_type::text:
+			return ctl_get_text( ctl, throw_if_unknown );
+			break;
+	}
+	return result;
+}
+bool module_impl::ctl_get_boolean( std::string_view ctl, bool throw_if_unknown ) const {
+	if ( !ctl.empty() ) {
+		// cppcheck false-positive
+		// cppcheck-suppress containerOutOfBounds
+		char rightmost = ctl.back();
+		if ( rightmost == '!' || rightmost == '?' ) {
+			if ( rightmost == '!' ) {
+				throw_if_unknown = true;
+			} else if ( rightmost == '?' ) {
+				throw_if_unknown = false;
+			}
+			ctl = ctl.substr( 0, ctl.length() - 1 );
+		}
+	}
+	auto found_ctl = std::find_if(get_ctl_infos().first, get_ctl_infos().second, [&](const ctl_info & info) -> bool { return info.name == ctl; });
+	if ( found_ctl == get_ctl_infos().second ) {
+		if ( ctl == "" ) {
+			throw openmpt::exception("empty ctl");
+		} else if ( throw_if_unknown ) {
+			throw openmpt::exception("unknown ctl: " + std::string(ctl));
+		} else {
+			return false;
+		}
+	}
+	if ( found_ctl->type != ctl_type::boolean ) {
+		throw openmpt::exception("wrong ctl value type");
+	}
 	if ( ctl == "" ) {
 		throw openmpt::exception("empty ctl");
 	} else if ( ctl == "load.skip_samples" || ctl == "load_skip_samples" ) {
-		return mpt::fmt::val( m_ctl_load_skip_samples );
+		return m_ctl_load_skip_samples;
 	} else if ( ctl == "load.skip_patterns" || ctl == "load_skip_patterns" ) {
-		return mpt::fmt::val( m_ctl_load_skip_patterns );
+		return m_ctl_load_skip_patterns;
 	} else if ( ctl == "load.skip_plugins" ) {
-		return mpt::fmt::val( m_ctl_load_skip_plugins );
+		return m_ctl_load_skip_plugins;
 	} else if ( ctl == "load.skip_subsongs_init" ) {
-		return mpt::fmt::val( m_ctl_load_skip_subsongs_init );
+		return m_ctl_load_skip_subsongs_init;
 	} else if ( ctl == "seek.sync_samples" ) {
-		return mpt::fmt::val( m_ctl_seek_sync_samples );
+		return m_ctl_seek_sync_samples;
+	} else if ( ctl == "render.resampler.emulate_amiga" ) {
+		return m_sndFile->m_Resampler.m_Settings.emulateAmiga;
+	} else {
+		MPT_ASSERT_NOTREACHED();
+		return false;
+	}
+}
+std::int64_t module_impl::ctl_get_integer( std::string_view ctl, bool throw_if_unknown ) const {
+	if ( !ctl.empty() ) {
+		// cppcheck false-positive
+		// cppcheck-suppress containerOutOfBounds
+		char rightmost = ctl.back();
+		if ( rightmost == '!' || rightmost == '?' ) {
+			if ( rightmost == '!' ) {
+				throw_if_unknown = true;
+			} else if ( rightmost == '?' ) {
+				throw_if_unknown = false;
+			}
+			ctl = ctl.substr( 0, ctl.length() - 1 );
+		}
+	}
+	auto found_ctl = std::find_if(get_ctl_infos().first, get_ctl_infos().second, [&](const ctl_info & info) -> bool { return info.name == ctl; });
+	if ( found_ctl == get_ctl_infos().second ) {
+		if ( ctl == "" ) {
+			throw openmpt::exception("empty ctl");
+		} else if ( throw_if_unknown ) {
+			throw openmpt::exception("unknown ctl: " + std::string(ctl));
+		} else {
+			return 0;
+		}
+	}
+	if ( found_ctl->type != ctl_type::integer ) {
+		throw openmpt::exception("wrong ctl value type");
+	}
+	if ( ctl == "" ) {
+		throw openmpt::exception("empty ctl");
 	} else if ( ctl == "subsong" ) {
-		return mpt::fmt::val( get_selected_subsong() );
+		return get_selected_subsong();
+	} else if ( ctl == "dither" ) {
+		return static_cast<int>( m_Dither->GetMode() );
+	} else {
+		MPT_ASSERT_NOTREACHED();
+		return 0;
+	}
+}
+double module_impl::ctl_get_floatingpoint( std::string_view ctl, bool throw_if_unknown ) const {
+	if ( !ctl.empty() ) {
+		// cppcheck false-positive
+		// cppcheck-suppress containerOutOfBounds
+		char rightmost = ctl.back();
+		if ( rightmost == '!' || rightmost == '?' ) {
+			if ( rightmost == '!' ) {
+				throw_if_unknown = true;
+			} else if ( rightmost == '?' ) {
+				throw_if_unknown = false;
+			}
+			ctl = ctl.substr( 0, ctl.length() - 1 );
+		}
+	}
+	auto found_ctl = std::find_if(get_ctl_infos().first, get_ctl_infos().second, [&](const ctl_info & info) -> bool { return info.name == ctl; });
+	if ( found_ctl == get_ctl_infos().second ) {
+		if ( ctl == "" ) {
+			throw openmpt::exception("empty ctl");
+		} else if ( throw_if_unknown ) {
+			throw openmpt::exception("unknown ctl: " + std::string(ctl));
+		} else {
+			return 0.0;
+		}
+	}
+	if ( found_ctl->type != ctl_type::floatingpoint ) {
+		throw openmpt::exception("wrong ctl value type");
+	}
+	if ( ctl == "" ) {
+		throw openmpt::exception("empty ctl");
+	} else if ( ctl == "play.tempo_factor" ) {
+		if ( !is_loaded() ) {
+			return 1.0;
+		}
+		return 65536.0 / m_sndFile->m_nTempoFactor;
+	} else if ( ctl == "play.pitch_factor" ) {
+		if ( !is_loaded() ) {
+			return 1.0;
+		}
+		return m_sndFile->m_nFreqFactor / 65536.0;
+	} else if ( ctl == "render.opl.volume_factor" ) {
+		return static_cast<double>( m_sndFile->m_OPLVolumeFactor ) / static_cast<double>( m_sndFile->m_OPLVolumeFactorScale );
+	} else {
+		MPT_ASSERT_NOTREACHED();
+		return 0.0;
+	}
+}
+std::string module_impl::ctl_get_text( std::string_view ctl, bool throw_if_unknown ) const {
+	if ( !ctl.empty() ) {
+		// cppcheck false-positive
+		// cppcheck-suppress containerOutOfBounds
+		char rightmost = ctl.back();
+		if ( rightmost == '!' || rightmost == '?' ) {
+			if ( rightmost == '!' ) {
+				throw_if_unknown = true;
+			} else if ( rightmost == '?' ) {
+				throw_if_unknown = false;
+			}
+			ctl = ctl.substr( 0, ctl.length() - 1 );
+		}
+	}
+	auto found_ctl = std::find_if(get_ctl_infos().first, get_ctl_infos().second, [&](const ctl_info & info) -> bool { return info.name == ctl; });
+	if ( found_ctl == get_ctl_infos().second ) {
+		if ( ctl == "" ) {
+			throw openmpt::exception("empty ctl");
+		} else if ( throw_if_unknown ) {
+			throw openmpt::exception("unknown ctl: " + std::string(ctl));
+		} else {
+			return std::string();
+		}
+	}
+	if ( ctl == "" ) {
+		throw openmpt::exception("empty ctl");
 	} else if ( ctl == "play.at_end" ) {
 		switch ( m_ctl_play_at_end )
 		{
@@ -1567,31 +1744,52 @@
 		default:
 			return std::string();
 		}
-	} else if ( ctl == "play.tempo_factor" ) {
-		if ( !is_loaded() ) {
-			return "1.0";
+	} else {
+		MPT_ASSERT_NOTREACHED();
+		return std::string();
+	}
+}
+
+void module_impl::ctl_set( std::string ctl, const std::string & value, bool throw_if_unknown ) {
+	if ( !ctl.empty() ) {
+		// cppcheck false-positive
+		// cppcheck-suppress containerOutOfBounds
+		char rightmost = ctl.back();
+		if ( rightmost == '!' || rightmost == '?' ) {
+			if ( rightmost == '!' ) {
+				throw_if_unknown = true;
+			} else if ( rightmost == '?' ) {
+				throw_if_unknown = false;
+			}
+			ctl = ctl.substr( 0, ctl.length() - 1 );
 		}
-		return mpt::fmt::val( 65536.0 / m_sndFile->m_nTempoFactor );
-	} else if ( ctl == "play.pitch_factor" ) {
-		if ( !is_loaded() ) {
-			return "1.0";
-		}
-		return mpt::fmt::val( m_sndFile->m_nFreqFactor / 65536.0 );
-	} else if ( ctl == "render.resampler.emulate_amiga" ) {
-		return mpt::fmt::val( m_sndFile->m_Resampler.m_Settings.emulateAmiga );
-	} else if ( ctl == "render.opl.volume_factor" ) {
-		return mpt::fmt::val( static_cast<double>( m_sndFile->m_OPLVolumeFactor ) / static_cast<double>( m_sndFile->m_OPLVolumeFactorScale ) );
-	} else if ( ctl == "dither" ) {
-		return mpt::fmt::val( static_cast<int>( m_Dither->GetMode() ) );
-	} else {
-		if ( throw_if_unknown ) {
-			throw openmpt::exception("unknown ctl: " + ctl);
+	}
+	auto found_ctl = std::find_if(get_ctl_infos().first, get_ctl_infos().second, [&](const ctl_info & info) -> bool { return info.name == ctl; });
+	if ( found_ctl == get_ctl_infos().second ) {
+		if ( ctl == "" ) {
+			throw openmpt::exception("empty ctl: := " + value);
+		} else if ( throw_if_unknown ) {
+			throw openmpt::exception("unknown ctl: " + ctl + " := " + value);
 		} else {
-			return std::string();
+			return;
 		}
 	}
+	switch ( found_ctl->type ) {
+		case ctl_type::boolean:
+			ctl_set_boolean( ctl, ConvertStrTo<bool>( value ), throw_if_unknown );
+			break;
+		case ctl_type::integer:
+			ctl_set_integer( ctl, ConvertStrTo<int64>( value ), throw_if_unknown );
+			break;
+		case ctl_type::floatingpoint:
+			ctl_set_floatingpoint( ctl, ConvertStrTo<double>( value ), throw_if_unknown );
+			break;
+		case ctl_type::text:
+			ctl_set_text( ctl, value, throw_if_unknown );
+			break;
+	}
 }
-void module_impl::ctl_set( std::string ctl, const std::string & value, bool throw_if_unknown ) {
+void module_impl::ctl_set_boolean( std::string_view ctl, bool value, bool throw_if_unknown ) {
 	if ( !ctl.empty() ) {
 		// cppcheck false-positive
 		// cppcheck-suppress containerOutOfBounds
@@ -1605,35 +1803,109 @@
 			ctl = ctl.substr( 0, ctl.length() - 1 );
 		}
 	}
+	auto found_ctl = std::find_if(get_ctl_infos().first, get_ctl_infos().second, [&](const ctl_info & info) -> bool { return info.name == ctl; });
+	if ( found_ctl == get_ctl_infos().second ) {
+		if ( ctl == "" ) {
+			throw openmpt::exception("empty ctl: := " + mpt::fmt::val( value ) );
+		} else if ( throw_if_unknown ) {
+			throw openmpt::exception("unknown ctl: " + std::string(ctl) + " := " + mpt::fmt::val(value));
+		} else {
+			return;
+		}
+	}
 	if ( ctl == "" ) {
-		throw openmpt::exception("empty ctl: := " + value);
+		throw openmpt::exception("empty ctl: := " + mpt::fmt::val( value ) );
 	} else if ( ctl == "load.skip_samples" || ctl == "load_skip_samples" ) {
-		m_ctl_load_skip_samples = ConvertStrTo<bool>( value );
+		m_ctl_load_skip_samples = value;
 	} else if ( ctl == "load.skip_patterns" || ctl == "load_skip_patterns" ) {
-		m_ctl_load_skip_patterns = ConvertStrTo<bool>( value );
+		m_ctl_load_skip_patterns = value;
 	} else if ( ctl == "load.skip_plugins" ) {
-		m_ctl_load_skip_plugins = ConvertStrTo<bool>( value );
+		m_ctl_load_skip_plugins = value;
 	} else if ( ctl == "load.skip_subsongs_init" ) {
-		m_ctl_load_skip_subsongs_init = ConvertStrTo<bool>( value );
+		m_ctl_load_skip_subsongs_init = value;
 	} else if ( ctl == "seek.sync_samples" ) {
-		m_ctl_seek_sync_samples = ConvertStrTo<bool>( value );
+		m_ctl_seek_sync_samples = value;
+	} else if ( ctl == "render.resampler.emulate_amiga" ) {
+		CResamplerSettings newsettings = m_sndFile->m_Resampler.m_Settings;
+		newsettings.emulateAmiga = value;
+		if ( newsettings != m_sndFile->m_Resampler.m_Settings ) {
+			m_sndFile->SetResamplerSettings( newsettings );
+		}
+	} else {
+		MPT_ASSERT_NOTREACHED();
+	}
+}
+void module_impl::ctl_set_integer( std::string_view ctl, std::int64_t value, bool throw_if_unknown ) {
+	if ( !ctl.empty() ) {
+		// cppcheck false-positive
+		// cppcheck-suppress containerOutOfBounds
+		char rightmost = ctl.back();
+		if ( rightmost == '!' || rightmost == '?' ) {
+			if ( rightmost == '!' ) {
+				throw_if_unknown = true;
+			} else if ( rightmost == '?' ) {
+				throw_if_unknown = false;
+			}
+			ctl = ctl.substr( 0, ctl.length() - 1 );
+		}
+	}
+	auto found_ctl = std::find_if(get_ctl_infos().first, get_ctl_infos().second, [&](const ctl_info & info) -> bool { return info.name == ctl; });
+	if ( found_ctl == get_ctl_infos().second ) {
+		if ( ctl == "" ) {
+			throw openmpt::exception("empty ctl: := " + mpt::fmt::val( value ) );
+		} else if ( throw_if_unknown ) {
+			throw openmpt::exception("unknown ctl: " + std::string(ctl) + " := " + mpt::fmt::val(value));
+		} else {
+			return;
+		}
+	}
+
+	if ( ctl == "" ) {
+		throw openmpt::exception("empty ctl: := " + mpt::fmt::val( value ) );
 	} else if ( ctl == "subsong" ) {
-		select_subsong( ConvertStrTo<int32>( value ) );
-	} else if ( ctl == "play.at_end" ) {
-		if ( value == "fadeout" ) {
-			m_ctl_play_at_end = song_end_action::fadeout_song;
-		} else if(value == "continue") {
-			m_ctl_play_at_end = song_end_action::continue_song;
-		} else if(value == "stop") {
-			m_ctl_play_at_end = song_end_action::stop_song;
+		select_subsong( mpt::saturate_cast<int32>( value ) );
+	} else if ( ctl == "dither" ) {
+		int dither = mpt::saturate_cast<int>( value );
+		if ( dither < 0 || dither >= NumDitherModes ) {
+			dither = DitherDefault;
+		}
+		m_Dither->SetMode( static_cast<DitherMode>( dither ) );
+	} else {
+		MPT_ASSERT_NOTREACHED();
+	}
+}
+void module_impl::ctl_set_floatingpoint( std::string_view ctl, double value, bool throw_if_unknown ) {
+	if ( !ctl.empty() ) {
+		// cppcheck false-positive
+		// cppcheck-suppress containerOutOfBounds
+		char rightmost = ctl.back();
+		if ( rightmost == '!' || rightmost == '?' ) {
+			if ( rightmost == '!' ) {
+				throw_if_unknown = true;
+			} else if ( rightmost == '?' ) {
+				throw_if_unknown = false;
+			}
+			ctl = ctl.substr( 0, ctl.length() - 1 );
+		}
+	}
+	auto found_ctl = std::find_if(get_ctl_infos().first, get_ctl_infos().second, [&](const ctl_info & info) -> bool { return info.name == ctl; });
+	if ( found_ctl == get_ctl_infos().second ) {
+		if ( ctl == "" ) {
+			throw openmpt::exception("empty ctl: := " + mpt::fmt::val( value ) );
+		} else if ( throw_if_unknown ) {
+			throw openmpt::exception("unknown ctl: " + std::string(ctl) + " := " + mpt::fmt::val(value));
 		} else {
-			throw openmpt::exception("unknown song end action:" + value);
+			return;
 		}
+	}
+
+	if ( ctl == "" ) {
+		throw openmpt::exception("empty ctl: := " + mpt::fmt::val( value ) );
 	} else if ( ctl == "play.tempo_factor" ) {
 		if ( !is_loaded() ) {
 			return;
 		}
-		double factor = ConvertStrTo<double>( value );
+		double factor = value;
 		if ( factor <= 0.0 || factor > 4.0 ) {
 			throw openmpt::exception("invalid tempo factor");
 		}
@@ -1643,33 +1915,58 @@
 		if ( !is_loaded() ) {
 			return;
 		}
-		double factor = ConvertStrTo<double>( value );
+		double factor = value;
 		if ( factor <= 0.0 || factor > 4.0 ) {
 			throw openmpt::exception("invalid pitch factor");
 		}
 		m_sndFile->m_nFreqFactor = mpt::saturate_round<uint32_t>( 65536.0 * factor );
 		m_sndFile->RecalculateSamplesPerTick();
-	} else if ( ctl == "render.resampler.emulate_amiga" ) {
-		CResamplerSettings newsettings = m_sndFile->m_Resampler.m_Settings;
-		newsettings.emulateAmiga = ConvertStrTo<bool>( value );
-		if ( newsettings != m_sndFile->m_Resampler.m_Settings ) {
-			m_sndFile->SetResamplerSettings( newsettings );
-		}
 	} else if ( ctl == "render.opl.volume_factor" ) {
-		m_sndFile->m_OPLVolumeFactor = mpt::saturate_round<int32>( ConvertStrTo<double>( value ) * static_cast<double>( m_sndFile->m_OPLVolumeFactorScale ) );
-	} else if ( ctl == "dither" ) {
-		int dither = ConvertStrTo<int>( value );
-		if ( dither < 0 || dither >= NumDitherModes ) {
-			dither = DitherDefault;
+		m_sndFile->m_OPLVolumeFactor = mpt::saturate_round<int32>( value * static_cast<double>( m_sndFile->m_OPLVolumeFactorScale ) );
+	} else {
+		MPT_ASSERT_NOTREACHED();
+	}
+}
+void module_impl::ctl_set_text( std::string_view ctl, std::string_view value, bool throw_if_unknown ) {
+	if ( !ctl.empty() ) {
+		// cppcheck false-positive
+		// cppcheck-suppress containerOutOfBounds
+		char rightmost = ctl.back();
+		if ( rightmost == '!' || rightmost == '?' ) {
+			if ( rightmost == '!' ) {
+				throw_if_unknown = true;
+			} else if ( rightmost == '?' ) {
+				throw_if_unknown = false;
+			}
+			ctl = ctl.substr( 0, ctl.length() - 1 );
 		}
-		m_Dither->SetMode( static_cast<DitherMode>( dither ) );
-	} else {
-		if ( throw_if_unknown ) {
-			throw openmpt::exception("unknown ctl: " + ctl + " := " + value);
+	}
+	auto found_ctl = std::find_if(get_ctl_infos().first, get_ctl_infos().second, [&](const ctl_info & info) -> bool { return info.name == ctl; });
+	if ( found_ctl == get_ctl_infos().second ) {
+		if ( ctl == "" ) {
+			throw openmpt::exception("empty ctl: := " + std::string( value ) );
+		} else if ( throw_if_unknown ) {
+			throw openmpt::exception("unknown ctl: " + std::string(ctl) + " := " + std::string(value));
 		} else {
-			// ignore
+			return;
 		}
 	}
+
+	if ( ctl == "" ) {
+		throw openmpt::exception("empty ctl: := " + std::string( value ) );
+	} else if ( ctl == "play.at_end" ) {
+		if ( value == "fadeout" ) {
+			m_ctl_play_at_end = song_end_action::fadeout_song;
+		} else if(value == "continue") {
+			m_ctl_play_at_end = song_end_action::continue_song;
+		} else if(value == "stop") {
+			m_ctl_play_at_end = song_end_action::stop_song;
+		} else {
+			throw openmpt::exception("unknown song end action:" + std::string(value));
+		}
+	} else {
+		MPT_ASSERT_NOTREACHED();
+	}
 }
 
 } // namespace openmpt
Index: libopenmpt/libopenmpt_impl.hpp
===================================================================
--- libopenmpt/libopenmpt_impl.hpp	(revision 12255)
+++ libopenmpt/libopenmpt_impl.hpp	(working copy)
@@ -15,6 +15,7 @@
 
 #include <iosfwd>
 #include <memory>
+#include <utility>
 
 #if defined(_MSC_VER)
 #pragma warning(push)
@@ -91,6 +92,17 @@
 
 	static const std::int32_t all_subsongs = -1;
 
+	enum class ctl_type {
+		boolean,
+		integer,
+		floatingpoint,
+		text,
+	};
+	struct ctl_info {
+		const char * name;
+		ctl_type type;
+	};
+
 	std::unique_ptr<log_interface> m_Log;
 	std::unique_ptr<log_forwarder> m_LogForwarder;
 	std::int32_t m_current_subsong;
@@ -130,8 +142,7 @@
 	static double could_open_probability( const OpenMPT::FileReader & file, double effort, std::unique_ptr<log_interface> log );
 public:
 	static std::vector<std::string> get_supported_extensions();
-	static bool is_extension_supported( const char * extension );
-	static bool is_extension_supported( const std::string & extension );
+	static bool is_extension_supported( std::string_view extension );
 	static double could_open_probability( callback_stream_wrapper stream, double effort, std::unique_ptr<log_interface> log );
 	static double could_open_probability( std::istream & stream, double effort, std::unique_ptr<log_interface> log );
 	static std::size_t probe_file_header_get_recommended_size();
@@ -202,9 +213,18 @@
 	std::string highlight_pattern_row_channel_command( std::int32_t p, std::int32_t r, std::int32_t c, int cmd ) const;
 	std::string format_pattern_row_channel( std::int32_t p, std::int32_t r, std::int32_t c, std::size_t width, bool pad ) const;
 	std::string highlight_pattern_row_channel( std::int32_t p, std::int32_t r, std::int32_t c, std::size_t width, bool pad ) const;
+	std::pair<const module_impl::ctl_info *, const module_impl::ctl_info *> get_ctl_infos() const;
 	std::vector<std::string> get_ctls() const;
 	std::string ctl_get( std::string ctl, bool throw_if_unknown = true ) const;
+	bool ctl_get_boolean( std::string_view ctl, bool throw_if_unknown = true ) const;
+	std::int64_t ctl_get_integer( std::string_view ctl, bool throw_if_unknown = true ) const;
+	double ctl_get_floatingpoint( std::string_view ctl, bool throw_if_unknown = true ) const;
+	std::string ctl_get_text( std::string_view ctl, bool throw_if_unknown = true ) const;
 	void ctl_set( std::string ctl, const std::string & value, bool throw_if_unknown = true );
+	void ctl_set_boolean( std::string_view ctl, bool value, bool throw_if_unknown = true );
+	void ctl_set_integer( std::string_view ctl, std::int64_t value, bool throw_if_unknown = true );
+	void ctl_set_floatingpoint( std::string_view ctl, double value, bool throw_if_unknown = true );
+	void ctl_set_text( std::string_view ctl, std::string_view value, bool throw_if_unknown = true );
 }; // class module_impl
 
 namespace helper {
Index: openmpt123/openmpt123.cpp
===================================================================
--- openmpt123/openmpt123.cpp	(revision 12255)
+++ openmpt123/openmpt123.cpp	(working copy)
@@ -711,33 +711,6 @@
 }
 
 
-template < typename T, typename Tmod >
-T ctl_get( Tmod & mod, const std::string & ctl ) {
-	T result = T();
-	try {
-		std::istringstream str;
-		str.imbue( std::locale::classic() );
-		str.str( mod.ctl_get( ctl ) );
-		str >> std::fixed >> std::setprecision(16) >> result;
-	} catch ( const openmpt::exception & ) {
-		// ignore
-	}
-	return result;
-}
-
-template < typename T, typename Tmod >
-void ctl_set( Tmod & mod, const std::string & ctl, const T & val ) {
-	try {
-		std::ostringstream str;
-		str.imbue( std::locale::classic() );
-		str << std::fixed << std::setprecision(16) << val;
-		mod.ctl_set( ctl, str.str() );
-	} catch ( const openmpt::exception & ) {
-		// ignore
-	}
-	return;
-}
-
 template < typename Tmod >
 static void apply_mod_settings( commandlineflags & flags, Tmod & mod ) {
 	flags.separation = std::max( flags.separation, std::int32_t(   0 ) );
@@ -753,12 +726,17 @@
 	mod.set_render_param( openmpt::module::RENDER_STEREOSEPARATION_PERCENT, flags.separation );
 	mod.set_render_param( openmpt::module::RENDER_INTERPOLATIONFILTER_LENGTH, flags.filtertaps );
 	mod.set_render_param( openmpt::module::RENDER_VOLUMERAMPING_STRENGTH, flags.ramping );
-	ctl_set( mod, "play.tempo_factor", tempo_flag_to_double( flags.tempo ) );
-	ctl_set( mod, "play.pitch_factor", pitch_flag_to_double( flags.pitch ) );
-	std::ostringstream dither_str;
-	dither_str.imbue( std::locale::classic() );
-	dither_str << flags.dither;
-	mod.ctl_set( "dither", dither_str.str() );
+	try {
+		mod.ctl_set_floatingpoint( "play.tempo_factor", tempo_flag_to_double( flags.tempo ) );
+	} catch ( const openmpt::exception & ) {
+		// ignore
+	}
+	try {
+		mod.ctl_set_floatingpoint( "play.pitch_factor", pitch_flag_to_double( flags.pitch ) );
+	} catch ( const openmpt::exception & ) {
+		// ignore
+	}
+	mod.ctl_set_integer( "dither", flags.dither );
 }
 
 struct prev_file { int count; prev_file( int c ) : count(c) { } };
Index: soundlib/Sndfile.h
===================================================================
--- soundlib/Sndfile.h	(revision 12255)
+++ soundlib/Sndfile.h	(working copy)
@@ -824,7 +824,7 @@
 	bool ReadWAV(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule);
 
 	static std::vector<const char *> GetSupportedExtensions(bool otherFormats);
-	static bool IsExtensionSupported(const char *ext); // UTF8, casing of ext is ignored
+	static bool IsExtensionSupported(std::string_view ext); // UTF8, casing of ext is ignored
 	static mpt::ustring ModContainerTypeToString(MODCONTAINERTYPE containertype);
 	static mpt::ustring ModContainerTypeToTracker(MODCONTAINERTYPE containertype);
 
Index: soundlib/Tables.cpp
===================================================================
--- soundlib/Tables.cpp	(revision 12255)
+++ soundlib/Tables.cpp	(working copy)
@@ -176,21 +176,19 @@
 }
 
 
-static bool IsEqualExtension(const char *a, const char *b)
+static bool IsEqualExtension(std::string_view a, std::string_view b)
 {
-	std::size_t lena = std::strlen(a);
-	std::size_t lenb = std::strlen(b);
-	if(lena != lenb)
+	if(a.length() != b.length())
 	{
 		return false;
 	}
-	return mpt::CompareNoCaseAscii(a, b, lena) == 0;
+	return mpt::CompareNoCaseAscii(a, b) == 0;
 }
 
 
-bool CSoundFile::IsExtensionSupported(const char *ext)
+bool CSoundFile::IsExtensionSupported(std::string_view ext)
 {
-	if(ext == nullptr || ext[0] == 0)
+	if(ext.length() == 0)
 	{
 		return false;
 	}
