diff --git a/project/lib/custom/openal/alc/alconfig.cpp b/project/lib/custom/openal/alc/alconfig.cpp new file mode 100644 index 0000000000..8d9c48f4dc --- /dev/null +++ b/project/lib/custom/openal/alc/alconfig.cpp @@ -0,0 +1,551 @@ +/** + * OpenAL cross platform audio library + * Copyright (C) 1999-2007 by authors. + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * Or go to http://www.gnu.org/copyleft/lgpl.html + */ + +#include "config.h" + +#include "alconfig.h" + +#ifdef _WIN32 +#include +#include +#endif +#ifdef __APPLE__ +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "almalloc.h" +#include "alnumeric.h" +#include "alstring.h" +#include "core/helpers.h" +#include "filesystem.h" +#include "fmt/ranges.h" +#include "gsl/gsl" +#include "strutils.hpp" + +#if ALSOFT_UWP +#include // !!This is important!! +#include +#include +#include +using namespace winrt; +#endif + +#if HAVE_CXXMODULES +import logging; +#else +#include "core/logging.h" +#endif + +namespace { + +using namespace std::string_view_literals; + +const auto EmptyString = std::string{}; + +struct ConfigEntry { + std::string key; + std::string value; + + ConfigEntry(auto&& key_, auto&& value_) + : key{std::forward(key_)}, value{std::forward(value_)} + { } +}; +std::vector ConfOpts; + + +/* True UTF-8 validation is way beyond me. However, this should weed out any + * obviously non-UTF-8 text. + * + * The general form of the byte stream is relatively simple. The first byte of + * a codepoint either has a 0 bit for the msb, indicating a single-byte ASCII- + * compatible codepoint, or the number of bytes that make up the codepoint + * (including itself) indicated by the number of successive 1 bits, and each + * successive byte of the codepoint has '10' for the top bits. That is: + * + * 0xxxxxxx - single-byte ASCII-compatible codepoint + * 110xxxxx 10xxxxxx - two-byte codepoint + * 1110xxxx 10xxxxxx 10xxxxxx - three-byte codepoint + * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - four-byte codepoint + * ... etc ... + * + * Where the 'x' bits are concatenated together to form a 32-bit Unicode + * codepoint. This doesn't check whether the codepoints themselves are valid, + * it just validates the correct number of bytes for multi-byte sequences. + */ +auto validate_utf8(const std::string_view str) -> bool +{ + auto const end = str.end(); + /* Look for the first multi-byte/non-ASCII codepoint. */ + auto current = std::ranges::find_if(str.begin(), end, + [](const char ch) -> bool { return (ch&0x80) != 0; }); + while(const auto remaining = std::distance(current, end)) + { + /* Get the number of bytes that make up this codepoint (must be at + * least 2). This includes the current byte. + */ + const auto tocheck = std::countl_one(as_unsigned(*current)); + if(tocheck < 2 || tocheck > remaining) + return false; + + const auto next = std::next(current, tocheck); + + /* Check that the following bytes are a proper continuation. */ + const auto valid = std::ranges::all_of(std::next(current), next, + [](const char ch) -> bool { return (ch&0xc0) == 0x80; }); + if(not valid) + return false; + + /* Seems okay. Look for the next multi-byte/non-ASCII codepoint. */ + current = std::ranges::find_if(next, end, [](const char ch) -> bool { return ch&0x80; }); + } + return true; +} + +auto lstrip(std::string &line) -> std::string& +{ + auto iter = std::ranges::find_if_not(line, [](const char c) { return std::isspace(c); }); + line.erase(line.begin(), iter); + return line; +} + +auto expdup(std::string_view str) -> std::string +{ + auto output = std::string{}; + + while(!str.empty()) + { + if(auto nextpos = str.find('$')) + { + output += str.substr(0, nextpos); + if(nextpos == std::string_view::npos) + break; + + str.remove_prefix(nextpos); + } + + str.remove_prefix(1); + if(str.empty()) + { + output += '$'; + break; + } + if(str.front() == '$') + { + output += '$'; + str.remove_prefix(1); + continue; + } + + const auto hasbraces = bool{str.front() == '{'}; + if(hasbraces) str.remove_prefix(1); + + const auto envenditer = std::ranges::find_if_not(str, + [](const char c) { return c == '_' || std::isalnum(c); }); + + if(hasbraces && (envenditer == str.end() || *envenditer != '}')) + continue; + + const auto envend = gsl::narrow(std::distance(str.begin(), envenditer)); + const auto envname = std::string{str.substr(0, envend)}; + str.remove_prefix(envend + hasbraces); + + if(const auto envval = al::getenv(envname.c_str())) + output += *envval; + } + + return output; +} + +auto GetConfigValue(const std::string_view devName, const std::string_view blockName, + const std::string_view keyName) -> const std::string& +{ + if(keyName.empty()) + return EmptyString; + + auto key = std::string{}; + if(!blockName.empty() && al::case_compare(blockName, "general"sv) != 0) + { + key = blockName; + key += '/'; + } + if(!devName.empty()) + { + key += devName; + key += '/'; + } + key += keyName; + + const auto iter = std::ranges::find(ConfOpts, key, &ConfigEntry::key); + if(iter != ConfOpts.cend()) + { + TRACE("Found option {} = \"{}\"", key, iter->value); + if(!iter->value.empty()) + return iter->value; + return EmptyString; + } + + if(devName.empty()) + return EmptyString; + return GetConfigValue({}, blockName, keyName); +} + +} // namespace + +void SetConfigValue(const std::string_view key, const std::string_view value) +{ + /* Check if we already have this option set */ + const auto ent = std::ranges::find(ConfOpts, key, &ConfigEntry::key); + if(ent != ConfOpts.end()) + { + if(!value.empty()) + ent->value = expdup(value); + else + ConfOpts.erase(ent); + } + else if(!value.empty()) + ConfOpts.emplace_back(std::move(key), expdup(value)); +} + +void LoadConfigFromFile(std::istream &f) +{ + constexpr auto whitespace_chars = " \t\n\f\r\v"sv; + + auto curSection = std::string{}; + auto buffer = std::string{}; + auto linenum = std::size_t{0}; + + while(std::getline(f, buffer)) + { + ++linenum; + if(lstrip(buffer).empty()) + continue; + + auto cmtpos = std::min(buffer.find('#'), buffer.size()); + if(cmtpos != 0) + cmtpos = buffer.find_last_not_of(whitespace_chars, cmtpos-1)+1; + if(cmtpos == 0) continue; + buffer.erase(cmtpos); + + if(not validate_utf8(buffer)) + { + ERR(" config parse error: non-UTF-8 characters on line {}", linenum); + ERR("{}", fmt::format(" {::#04x}", + buffer|std::views::transform([](auto c) { return as_unsigned(c); }))); + continue; + } + + if(buffer[0] == '[') + { + const auto endpos = buffer.find(']', 1); + if(endpos == 1 || endpos == std::string::npos) + { + ERR(" config parse error on line {}: bad section \"{}\"", linenum, buffer); + continue; + } + if(const auto last = buffer.find_first_not_of(whitespace_chars, endpos+1); + last < buffer.size() && buffer[last] != '#') + { + ERR(" config parse error on line {}: extraneous characters after section \"{}\"", + linenum, buffer); + continue; + } + + auto section = std::string_view{buffer}.substr(1, endpos-1); + + curSection.clear(); + if(al::case_compare(section, "general"sv) == 0) + continue; + + while(!section.empty()) + { + const auto nextp = section.find('%'); + if(nextp == std::string_view::npos) + { + curSection += section; + break; + } + + curSection += section.substr(0, nextp); + section.remove_prefix(nextp); + + if(section.size() > 2 + && ((section[1] >= '0' && section[1] <= '9') + || (section[1] >= 'a' && section[1] <= 'f') + || (section[1] >= 'A' && section[1] <= 'F')) + && ((section[2] >= '0' && section[2] <= '9') + || (section[2] >= 'a' && section[2] <= 'f') + || (section[2] >= 'A' && section[2] <= 'F'))) + { + auto b = 0; + if(section[1] >= '0' && section[1] <= '9') + b = (section[1]-'0') << 4; + else if(section[1] >= 'a' && section[1] <= 'f') + b = (section[1]-'a'+0xa) << 4; + else if(section[1] >= 'A' && section[1] <= 'F') + b = (section[1]-'A'+0x0a) << 4; + if(section[2] >= '0' && section[2] <= '9') + b |= section[2]-'0'; + else if(section[2] >= 'a' && section[2] <= 'f') + b |= section[2]-'a'+0xa; + else if(section[2] >= 'A' && section[2] <= 'F') + b |= section[2]-'A'+0x0a; + curSection += gsl::narrow_cast(b); + section.remove_prefix(3); + } + else if(section.size() > 1 && section[1] == '%') + { + curSection += '%'; + section.remove_prefix(2); + } + else + { + curSection += '%'; + section.remove_prefix(1); + } + } + + continue; + } + + auto sep = buffer.find('='); + if(sep == std::string::npos) + { + ERR(" config parse error on line {}: malformed option \"{}\"", linenum, buffer); + continue; + } + auto keypart = std::string_view{buffer}.substr(0, sep++); + keypart.remove_suffix(keypart.size() - (keypart.find_last_not_of(whitespace_chars)+1)); + if(keypart.empty()) + { + ERR(" config parse error on line {}: malformed option \"{}\"", linenum, buffer); + continue; + } + auto valpart = std::string_view{buffer}.substr(sep); + valpart.remove_prefix(std::min(valpart.find_first_not_of(whitespace_chars), + valpart.size())); + + auto fullKey = curSection; + if(!fullKey.empty()) + fullKey += '/'; + fullKey += keypart; + + if(valpart.size() > size_t{std::numeric_limits::max()}) + { + ERR(" config parse error on line {}: value too long \"{}\"", linenum, buffer); + continue; + } + if(valpart.size() > 1) + { + if((valpart.front() == '"' && valpart.back() == '"') + || (valpart.front() == '\'' && valpart.back() == '\'')) + { + valpart.remove_prefix(1); + valpart.remove_suffix(1); + } + } + + TRACE(" setting '{}' = '{}'", fullKey, valpart); + + SetConfigValue (fullKey, valpart); + } + ConfOpts.shrink_to_fit(); +} + +void FunkinALConfigDefault() +{ + SetConfigValue("frequency", "48000"); + SetConfigValue("sample-type", "float32"); + SetConfigValue("stereo-mode", "speakers"); + SetConfigValue("stereo-encoding", "basic"); + SetConfigValue("cf_level", "0"); + SetConfigValue("output-limiter", "false"); + SetConfigValue("front-stablizer", "false"); + SetConfigValue("volume-adjust", "0"); + SetConfigValue("period_size", "480"); + SetConfigValue("periods", "4"); + SetConfigValue("sends", "64"); + SetConfigValue("dither", "false"); + SetConfigValue("dither-depth", "0"); + SetConfigValue("decoder/hq-mode", "false"); + SetConfigValue("decoder/distance-comp", "false"); + SetConfigValue("decoder/nfc", "false"); + // exclusive-mode removes the latency about 20ms (from 80ms to 40ms in my testing -ralty) + // but game recorders wont be able to record the game because of the audio mixer being exclusive and not shared. + SetConfigValue("wasapi/exclusive-mode", "false"); + SetConfigValue("wasapi/spatial-api", "false"); + SetConfigValue("wasapi/allow-resampler", "false"); + ConfOpts.shrink_to_fit(); +} + +void ReadALConfig() +{ + FunkinALConfigDefault(); + + #ifdef _WIN32 + + auto path = fs::path{}; + + path = fs::path(al::char_as_u8(GetProcBinary().path)); + if(!path.empty()) + { + path /= L"alsoft.ini"; + TRACE("Loading config {}...", al::u8_as_char(path.u8string())); + if(auto f = fs::ifstream{path}; f.is_open()) + LoadConfigFromFile(f); + } + + if(auto confpath = al::getenv(L"ALSOFT_CONF")) + { + path = *confpath; + TRACE("Loading config {}...", al::u8_as_char(path.u8string())); + if(auto f = fs::ifstream{path}; f.is_open()) + LoadConfigFromFile(f); + } + + #else + + auto path = fs::path{"/etc/openal/alsoft.conf"}; + + path = GetProcBinary().path; + if(!path.empty()) + { + path /= "alsoft.conf"; + + TRACE("Loading config {}...", al::u8_as_char(path.u8string())); + if(auto f = std::ifstream{path}; f.is_open()) + LoadConfigFromFile(f); + } + + if(auto confname = al::getenv("ALSOFT_CONF")) + { + TRACE("Loading config {}...", *confname); + if(auto f = std::ifstream{*confname}; f.is_open()) + LoadConfigFromFile(f); + } + + #endif +} + +auto ConfigValueStr(const std::string_view devName, const std::string_view blockName, + const std::string_view keyName) -> std::optional +{ + if(auto&& val = GetConfigValue(devName, blockName, keyName); !val.empty()) + return val; + return std::nullopt; +} + +auto ConfigValueI32(std::string_view const devName, std::string_view const blockName, + std::string_view const keyName) -> std::optional +{ + if(auto&& val = GetConfigValue(devName, blockName, keyName); !val.empty()) try { + return std::stoi(val, nullptr, 0); + } + catch(std::out_of_range&) { + WARN("Option is out of range of i32: {} = {}", keyName, val); + } + catch(std::exception&) { + WARN("Option is not an i32: {} = {}", keyName, val); + } + + return std::nullopt; +} + +auto ConfigValueU32(std::string_view const devName, std::string_view const blockName, + std::string_view const keyName) -> std::optional +{ + if(auto&& val = GetConfigValue(devName, blockName, keyName); !val.empty()) try { + return gsl::narrow(std::stoul(val, nullptr, 0)); + } + catch(std::out_of_range&) { + WARN("Option is out of range of u32: {} = {}", keyName, val); + } + catch(gsl::narrowing_error&) { + WARN("Option is out of range of u32: {} = {}", keyName, val); + } + catch(std::exception&) { + WARN("Option is not an u32: {} = {}", keyName, val); + } + return std::nullopt; +} + +auto ConfigValueF32(std::string_view const devName, std::string_view const blockName, + std::string_view const keyName) -> std::optional +{ + if(auto&& val = GetConfigValue(devName, blockName, keyName); !val.empty()) try { + return std::stof(val); + } + catch(std::exception&) { + WARN("Option is not a float: {} = {}", keyName, val); + } + return std::nullopt; +} + +auto ConfigValueBool(std::string_view const devName, std::string_view const blockName, + std::string_view const keyName) -> std::optional +{ + if(auto&& val = GetConfigValue(devName, blockName, keyName); !val.empty()) try { + return al::case_compare(val, "on"sv) == 0 || al::case_compare(val, "yes"sv) == 0 + || al::case_compare(val, "true"sv) == 0 || std::stoll(val) != 0; + } + catch(std::out_of_range&) { + /* If out of range, the value is some non-0 (true) value and it doesn't + * matter that it's too big or small. + */ + return true; + } + catch(std::exception&) { + /* If stoll fails to convert for any other reason, it's some other word + * that's treated as false. + */ + return false; + } + return std::nullopt; +} + +auto GetConfigValueBool(const std::string_view devName, const std::string_view blockName, + const std::string_view keyName, bool def) -> bool +{ + if(auto&& val = GetConfigValue(devName, blockName, keyName); !val.empty()) try { + return al::case_compare(val, "on"sv) == 0 || al::case_compare(val, "yes"sv) == 0 + || al::case_compare(val, "true"sv) == 0 || std::stoll(val) != 0; + } + catch(std::out_of_range&) { + return true; + } + catch(std::exception&) { + return false; + } + return def; +} diff --git a/project/lib/openal-files.xml b/project/lib/openal-files.xml index c021774e52..3e1d7d8f17 100644 --- a/project/lib/openal-files.xml +++ b/project/lib/openal-files.xml @@ -184,7 +184,7 @@
- + diff --git a/src/lime/media/AudioManager.hx b/src/lime/media/AudioManager.hx index c755401941..ad0e8f43f3 100644 --- a/src/lime/media/AudioManager.hx +++ b/src/lime/media/AudioManager.hx @@ -2,12 +2,6 @@ package lime.media; import lime.system.CFFIPointer; import haxe.MainLoop; -#if (windows || mac || linux || android || ios) -import haxe.io.Path; -import lime.system.System; -import sys.FileSystem; -import sys.io.File; -#end import haxe.Timer; import lime._internal.backend.native.NativeCFFI; import lime.media.openal.AL; @@ -42,10 +36,6 @@ class AudioManager #if !lime_doc_gen if (context.type == OPENAL) { - #if (windows || mac || linux || android || ios) - setupConfig(); - #end - var alc = context.openal; var device = alc.openDevice(); if (device != null) @@ -171,44 +161,4 @@ class AudioManager #end } #end - - @:noCompletion - private static function setupConfig():Void - { - #if (lime_openal && (windows || mac || linux || android || ios)) - final alConfig:Array = []; - - alConfig.push('[general]'); - alConfig.push('channels=stereo'); - alConfig.push('sample-type=float32'); - alConfig.push('stereo-mode=speakers'); - alConfig.push('stereo-encoding=panpot'); - alConfig.push('hrtf=false'); - alConfig.push('cf_level=0'); - alConfig.push('resampler=fast_bsinc24'); - alConfig.push('front-stablizer=false'); - alConfig.push('output-limiter=false'); - alConfig.push('volume-adjust=0'); - alConfig.push('period_size=441'); - - alConfig.push('[decoder]'); - alConfig.push('hq-mode=false'); - alConfig.push('distance-comp=false'); - alConfig.push('nfc=false'); - - try - { - final directory:String = Path.directory(Path.withoutExtension(System.applicationStorageDirectory)); - final path:String = Path.join([directory, #if windows 'audio-config.ini' #else 'audio-config.conf' #end]); - final content:String = alConfig.join('\n'); - - if (!FileSystem.exists(directory)) FileSystem.createDirectory(directory); - - if (!FileSystem.exists(path)) File.saveContent(path, content); - - Sys.putEnv('ALSOFT_CONF', path); - } - catch (e:Dynamic) {} - #end - } }