Skip to content

Commit

Permalink
introduce strict separation of global/file/cmdline configuration
Browse files Browse the repository at this point in the history
Lots of required plumbing work here! There are now multiple Config
instances, and each of them keeps track of which items are set
within it. The final configuration is then built from the disjoint
sets.

There's no user-visible feature yet, except that the order of
configuration item precedence has been changed slightly: Global
settings in tm.ini in the current module's directory no longer
override per-file settings in the program directory's tm.ini.
Furthermore, .tm files now must not contain any file-specific
sections. (There wasn't any point in having those before, but
now they would be outright ignored if present.)
  • Loading branch information
kajott committed Oct 23, 2024
1 parent 53d0ef1 commit 2c886c6
Show file tree
Hide file tree
Showing 11 changed files with 1,084 additions and 307 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ add_executable (tm
src/textarea.cpp
src/pathutil.cpp
src/renderer.cpp
src/numset.cpp
font/font_data.cpp
logo/logo_data.cpp
)
Expand Down
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,22 +73,27 @@ The following aspects can be configured:

All items can be changed at runtime when loading a new module or pressing the **F5** key, _except_ those marked with an asterisk (*); these are only evaluated once on startup.

The following locations are searched for configuration files:
- `tm.ini` in the program's directory (i.e. directly next to `tm.exe`)
- `tm.ini` in the currently opened module file's directory
- a file with the same name as the currently opened module file, but with an extra suffix of `.tm`; for example, for `foo.mod`, the configuration file will be `foo.mod.tm`

Configuration files are processed in this exact order, line by line. Options specified later override options specified earlier.

The configuration files can contain multiple sections, delimited by lines containing the section name in square brackets, `[like so]`. The following sections are evaluated, and all other sections are ignored:
- the unnamed section at the beginning of the file
- the `[TrackMeister]` or `[TM]` sections
- sections that match the current module's file name, e.g. `[foo*.mod]`;
- **global** settings are loaded from ...
- the unnamed section at the beginning of the file
- sections called `[TrackMeister]` or `[TM]`
- **file-specific** settings are loaded from
sections that match the current module's file name, e.g. `[foo*.mod]`;
the following rules apply for those:
- only the filename is matched, no directory names
- matching is case-insensitive
- exactly one '`*`' may be used as a wildcard (no '`?`'s, no multiple '`*`'s)

The following files and sections are searched for configuration, in this order, line by line, with later options overriding earlier ones:
- global settings from `tm.ini` in the program's directory
(i.e. directly next to `tm.exe`)
- global settings from `tm.ini` in the currently opened module file's directory
- file-specific settings from `tm.ini` in the program directory
- file-specific settings from `tm.ini` in the module directory
- any settings from a file with the same name as the currently opened module file, but with an extra suffix of `.tm`; for example, for `foo.mod`, the configuration file will be `foo.mod.tm`
- these are parsed like global settings (i.e. there's no need to have any section markers in the `.tm` files), but they are, of course, handled as file-specific settings


All other lines contain key-value pairs of the form "`key = value`" or "`key: value`". Spaces, dashes (`-`) and underscores (`_`) in key names are ignored. All parts of a line following a semicolon (`;`) are ignored. It's allowed to put comments at the end of key/value lines.

To get a list of all possible settings, along with documentation and the default values for each setting, run TrackMeister normally and press **Ctrl+Shift+S**, or run `tm --save-default-config` from a command line. This will generate a file `tm_default.ini` in the current directory (usually the program directory) that also be used as a template for an individual configuration.
Expand Down
27 changes: 20 additions & 7 deletions src/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ int Application::init(int argc, char* argv[]) {
m_mainIniFile.assign(argv[0]);
PathUtil::dirnameInplace(m_mainIniFile);
PathUtil::joinInplace(m_mainIniFile, "tm.ini");
m_config.load(m_mainIniFile.c_str());
m_config.load(m_cmdline);
m_globalConfig.load(m_mainIniFile.c_str());
m_cmdlineConfig.load(m_cmdline);
updateConfig();

// initialize everything
m_sys.initVideo(baseWindowTitle,
Expand Down Expand Up @@ -117,6 +118,13 @@ bool Application::hasTrackNumber(const char* basename) {
&& ((basename[2] == '-') || (basename[2] == '_') || (basename[2] == ' '));
}

void Application::updateConfig() {
m_config.reset();
m_config.import(m_globalConfig);
m_config.import(m_fileConfig);
m_config.import(m_cmdlineConfig);
}

////////////////////////////////////////////////////////////////////////////////

///// event handlers and related code
Expand Down Expand Up @@ -840,11 +848,16 @@ bool Application::loadModule(const char* path, bool forScanning) {
std::string filename(PathUtil::basename(m_fullpath));

// load configuration files
m_config.reset();
m_config.load(m_mainIniFile.c_str(), filename.c_str());
m_config.load(PathUtil::join(PathUtil::dirname(m_fullpath), "tm.ini").c_str(), filename.c_str());
m_config.load((m_fullpath + ".tm").c_str());
m_config.load(m_cmdline);
m_dirIniFile = PathUtil::join(PathUtil::dirname(m_fullpath), "tm.ini");
m_fileIniFile = m_fullpath + ".tm";
m_globalConfig.reset();
m_globalConfig.load(m_mainIniFile.c_str());
m_globalConfig.load(m_dirIniFile.c_str());
m_fileConfig.reset();
m_fileConfig.load(m_mainIniFile.c_str(), filename.c_str());
m_fileConfig.load(m_dirIniFile.c_str(), filename.c_str());
m_fileConfig.load(m_fileIniFile.c_str());
updateConfig();
updateImages();
m_renderer.setAlphaGamma(m_config.alphaGamma);

Expand Down
8 changes: 7 additions & 1 deletion src/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ class Application {
// core data
SystemInterface& m_sys;
TextBoxRenderer m_renderer;
Config m_config;
Config::PreparedCommandLine m_cmdline;
Config m_globalConfig;
Config m_fileConfig;
Config m_cmdlineConfig;
Config m_config;
int m_sampleRate;
bool m_scanning = false;
std::atomic_bool m_cancelScanning = false;
Expand All @@ -42,6 +45,8 @@ class Application {
std::vector<uint32_t> m_playableExts;
std::thread* m_scanThread = nullptr;
std::string m_mainIniFile;
std::string m_dirIniFile;
std::string m_fileIniFile;
float m_instanceGain = 0.0f;

// metadata
Expand Down Expand Up @@ -156,6 +161,7 @@ class Application {
int toPixels(int value) const;
int toTextSize(int value) const;
int textWidth(int size, const char* text) const;
void updateConfig();
void updateImages();
void updateImage(ExternalImage& img, const std::string& path, int channels, const char* what);
void updateLayout(bool resetBoxVisibility=false);
Expand Down
28 changes: 23 additions & 5 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ bool Config::load(const char* filename, const char* matchName) {
ctx.filename.assign(filename);
ctx.lineno = 0;
bool ignoreNextLineSegment = false;
bool validSection = true;
bool validSection = !matchName;
while (std::fgets(line, LineBufferSize, f)) {
// check end of line
if (!line[0]) { break; /* EOF */ }
Expand Down Expand Up @@ -200,8 +200,9 @@ bool Config::load(const char* filename, const char* matchName) {
if (!key) { continue; }
if ((key[0] == '[') && !value && (end >= key) && (end[-1] == ']')) {
++key; end[-1] = '\0';
validSection = stringEqualEx(key, "TrackMeister") || stringEqualEx(key, "TM")
|| (matchName && PathUtil::matchFilename(key, matchName));
validSection = (matchName && matchName[0])
? PathUtil::matchFilename(key, matchName)
: (stringEqualEx(key, "TrackMeister") || stringEqualEx(key, "TM"));
Dprintf(" - %s section '%s'\n", validSection ? "parsing" : "ignoring", key);
continue;
}
Expand All @@ -213,6 +214,7 @@ bool Config::load(const char* filename, const char* matchName) {
for (item = g_ConfigItems; item->name; ++item) {
if (stringEqualEx(item->name, key)) {
item->setter(ctx, *this, value);
set.add(item->ordinal);
break;
}
}
Expand Down Expand Up @@ -257,6 +259,7 @@ void Config::load(const Config::PreparedCommandLine& cmdline) {
for (item = g_ConfigItems; item->name; ++item) {
if (stringEqualEx(item->name, key.c_str())) {
item->setter(ctx, *this, arg.substr(sep + 1u).c_str());
set.add(item->ordinal);
break;
}
}
Expand All @@ -273,8 +276,12 @@ bool Config::save(const char* filename) {
if (!f) { return false; }
bool res = (fwrite(g_DefaultConfigFileIntro, std::strlen(g_DefaultConfigFileIntro), 1, f) == 1);
for (const ConfigItem *item = g_ConfigItems; item->name; ++item) {
if (item->newGroup) { fprintf(f, "\n"); }
fprintf(f, "%s = %-10s ; %s\n", item->name, item->getter(*this).c_str(), item->description);
if (item->flags & ConfigItem::Flags::NewGroup) { fprintf(f, "\n"); }
std::string name(item->name);
if (int(name.size()) < g_ConfigItemMaxNameLength) {
name.resize(g_ConfigItemMaxNameLength, ' ');
}
fprintf(f, "%s = %-10s ; %s\n", name.c_str(), item->getter(*this).c_str(), item->description);
}
fclose(f);
return res;
Expand All @@ -290,3 +297,14 @@ bool Config::saveLoudness(const char* filename) {
fclose(f);
return true;
}

////////////////////////////////////////////////////////////////////////////////

void Config::import(const Config& src) {
for (const ConfigItem *item = g_ConfigItems; item->name; ++item) {
if (src.set.contains(item->ordinal)) {
item->copy(src, *this);
}
}
set.update(src.set);
}
30 changes: 17 additions & 13 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include <list>
#include <string>

#include "numset.h"

enum class FilterMethod {
None = 0, //!< OpenMPT INTERPOLATIONFILTER_LENGTH = 1
Linear, //!< OpenMPT INTERPOLATIONFILTER_LENGTH = 2
Expand Down Expand Up @@ -39,23 +41,23 @@ constexpr bool isValidLoudness(float db) { return (db > -100.0f); }
//! color is assumed to be fully opaque.
//! In code, all colors are in the format 0xAABBGGRRu.
struct Config {
bool fullscreen = false; //!< whether to run in fullscreen mode
int windowWidth = 1920; //!< initial window width in non-fullscreen mode, in pixels
int windowHeight = 1080; //!< initial window height in non-fullscreen mode, in pixels
bool fullscreen = false; //!< whether to run in fullscreen mode [startup]
int windowWidth = 1920; //!< initial window width in non-fullscreen mode, in pixels [startup]
int windowHeight = 1080; //!< initial window height in non-fullscreen mode, in pixels [startup]
float alphaGamma = 2.2f; //!< fake gamma-correct rendering by applying gamma to the alpha channel; higher values = thicker and less aliasing for bright-on-dark text

int sampleRate = 48000; //!< audio sampling rate
int audioBufferSize = 512; //!< size of the audio buffer, in samples; if there are dropouts, try doubling this value
FilterMethod filter = FilterMethod::Auto; //!< audio resampling filter to be used
int stereoSeparation = 100; //!< amount of stereo separation, in percent (0 = mono, 100 = half stereo for MOD / full stereo for others, 200 = full stereo for MOD)
int volumeRamping = -1; //!< volume ramping strength (0 = no ramping, 10 = softest ramping, -1 = recommended default)
float gain = 0.0f; //!< global gain to apply, in decibels
float loudness = InvalidLoudness; //!< the current track's measured loudness, in decibels; values < -100 mean "no loudness measured"
float targetLoudness = -18.0f; //!< target loudness, in decibels (or LUFS); if the 'loudness' parameter is valid, an extra gain will be applied (in addition to 'gain') so that the loudness is corrected to this value
int sampleRate = 48000; //!< audio sampling rate [startup]
int audioBufferSize = 512; //!< size of the audio buffer, in samples; if there are dropouts, try doubling this value [startup]
FilterMethod filter = FilterMethod::Auto; //!< audio resampling filter to be used [reload]
int stereoSeparation = 100; //!< amount of stereo separation, in percent (0 = mono, 100 = half stereo for MOD / full stereo for others, 200 = full stereo for MOD) [reload]
int volumeRamping = -1; //!< volume ramping strength (0 = no ramping, 10 = softest ramping, -1 = recommended default) [reload]
float gain = 0.0f; //!< global gain to apply, in decibels [reload]
float loudness = InvalidLoudness; //!< the current track's measured loudness, in decibels; values < -100 mean "no loudness measured" [hidden]
float targetLoudness = -18.0f; //!< target loudness, in decibels (or LUFS); if the 'loudness' parameter is valid, an extra gain will be applied (in addition to 'gain') so that the loudness is corrected to this value [reload]

bool autoPlay = true; //!< automatically start playing when loading a module; you may want to turn this off for actual competitions
bool autoPlay = true; //!< automatically start playing when loading a module; you may want to turn this off for actual competitions [reload]
bool autoAdvance = false; //!< automatically continue with the next song in the directory if the current song stopped; allows for jukebox-like functionality
bool shuffle = false; //!< play tracks of the directory endlessly, and in random order
bool shuffle = false; //!< play tracks of the directory endlessly, and in random order [global]
bool loop = false; //!< whether to loop the song after it's finished, or play the song's programmed loop if it there is one
bool fadeOutAfterLoop = false; //!< whether to trigger a slow fade-out after the song looped
float fadeOutAt = 0.0f; //!< number of seconds after which the song shall be slowly faded out automatically (0 = no auto-fade)
Expand Down Expand Up @@ -185,10 +187,12 @@ struct Config {
uint32_t toastTextColor = 0xFFFFFFFFu; //!< text color of a "toast" status message
float toastDuration = 2.0f; //!< time a "toast" status message shall be visible until it's completely faded out

NumberSet set;
inline Config() {}
inline void reset() { Config defaultConfig; *this = defaultConfig; }
using PreparedCommandLine = std::list<std::string>;
static PreparedCommandLine prepareCommandLine(int& argc, char** argv);
void import(const Config& src);
void load(const PreparedCommandLine& cmdline);
bool load(const char* filename, const char* matchName=nullptr);
bool save(const char* filename);
Expand Down
Loading

0 comments on commit 2c886c6

Please sign in to comment.