#### Benben
#### Copyright (C) 2023 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or
#### modify it under the terms of the GNU Affero General Public
#### License as published by the Free Software Foundation, either
#### version 3 of the License, or (at your option) any later version.
####
#### This program 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
#### Affero General Public License for more details.
####
#### You should have received a copy of the GNU Affero General Public License
#### along with this program. If not, see <https://www.gnu.org/licenses/>.
require "libremiliacr"
require "remiaudio"
require "yunosynth"
#require "remislang"
require "./common"
require "./vgm-extensions"
require "./command-line"
require "./playbacksettings"
require "./config"
require "./file-handler"
# Load all UIs
require "./printer"
require "./uis/orig/*"
require "./audio-driver"
require "./player"
require "./rendering/renderer"
require "./playback-manager"
####
#### Main Toplevel Program
####
module Benben
RemiLib.classSetonce! ui : Benben::UI?
RemiLib.classSetonce! fileHandler : Benben::FileHandler?
RemiLib.classSetonce! driver : Benben::AudioDriver?
RemiLib.classSetonce! player : Benben::Player?
RemiLib.classSetonce! manager : PlaybackManager?
RemiLib.classSetonce! logFile : File?
# Used to find the configuration file and data files.
@@configResolver = RemiConf::Resolver.xdg(SHORT_NAME)
class_getter configResolver
# The loaded configuration file.
@@config : Config? = nil
class_property! config
# Holds the original screen attributes.
@@originalAttrs : LibC::Termios?
class_property originalAttrs
# Restore the original screen attributes on exit. S-Lang sometimes doesn't do
# this correctly.
at_exit do
Benben.originalAttrs.try do |attrs|
LibC.tcsetattr(STDOUT.fd, LibC::TCSANOW, pointerof(attrs))
end
end
RemiLib.classSetonce! settings : PlaybackSettings?
class Program
def initialize
LibC.tcgetattr(STDOUT.fd, out origAttrs)
Benben.originalAttrs = origAttrs
# This is meant for debugging, and is currently only useful if Benben is
# built with -Dbenben_debug
if filename = ENV["BENBEN_DEBUG_LOG_FILE"]?
mode = "w"
if filename.starts_with?("append:")
mode = "a"
filename = filename[7..].strip
else
filename = filename.strip
end
unless filename.empty?
begin
Benben.logFile = File.open(filename, mode)
RemiLib.log.defaultStream = Benben.logFile
RemiLib.log.debugStream = Benben.logFile
Benben.logFile.sync = true
RemiLib.log.dlog!("=== Benben Debug Log File, opened #{Time.local} ===")
rescue err : Exception
RemiLib.log.fatal("Couldn't open the requested log file: #{err}")
end
else
RemiLib.log.fatal("Couldn't open the requested log file: no file specified")
end
end
# Ensure we have a minimum number of worker threads.
Benben.dlog!("CRYSTAL_WORKERS: #{ENV["CRYSTAL_WORKERS"]?}")
if crWorkers = ENV["CRYSTAL_WORKERS"]?
if crWorkers.to_i32 < 4
RemiLib.log.fatal("Could not request at least four CRYSTAL_WORKERS from the Crystal runtime")
end
end
# Initialize and parse the command line
initCLI
Benben.args.parse
# Load the configuration files
Benben.config = Config.load
Benben.songConfigs = SongConfig.load
# Check for arguments that may exit the program early. This will also
# check their values.
checkEarlyOutArguments
# We require at least one VGM file
if Benben.args.positionalArgs.empty?
RemiLib.log.error("No VGM files specified")
exit 1
end
# If we're rendering, make sure the bit depth is good
checkRenderingArgs
Benben.settings = PlaybackSettings.new
Benben.settings.apply(Benben.config)
Benben.settings.apply(Benben.args)
end
# Initializes the file handler.
private def initFileHandler : Nil
if (Benben.args["render"].called? && !Benben.args["quiet"].called?) || !Benben.args["render"].called?
puts "Checking files..."
end
STDOUT.flush
Benben.fileHandler = FileHandler.new
if (Benben.args["render"].called? && !Benben.args["quiet"].called?) || !Benben.args["render"].called?
format("~:d file~:p queued for playback~%", Benben.fileHandler.size)
end
# Do we have files to play?
if Benben.fileHandler.size == 0
RemiLib.log.error("No files to play")
exit 1
end
end
# Starts the rendering process.
private def runRendering
# Start the renderer
Benben::Rendering::Renderer.new.render
end
# Starts normal (non-rendering) operation.
private def runNormal
# Initialize audio driver. We do this before starting up the printer
# because some drivers may print extra info that isn't related directly to
# this program.
drv = if Benben.args["driver"].called?
Driver.parse(Benben.args["driver"].str)
else
Benben.config.audioDriver
end
begin
# Initialize audio driver (i.e., the backend used for output)
Benben.driver = AudioDriver.new(drv)
# Check a few args that should be used with --render (not considered a
# fatal error).
if Benben.args["outdir"].called?
RemiLib.log.warn("--outdir used, but --render was not specified")
end
if Benben.args["quiet"].called?
RemiLib.log.warn("--quiet used, but --render was not specified")
end
# Create a PlaybackManager
Benben.manager = PlaybackManager.new
# TODO move this earlier once the global PlaybackSettings is also used for rendering.
@@config = nil # No longer needed since everything is in the global PlaybackSettings.
# Start the PlaybackManager
Benben.manager.run
ensure
Benben.dlog!("Within main ensure, deinitializing stuff")
# Clean up driver resources
Benben.driver?.try &.deinit
# Flush
STDOUT.flush
STDOUT << '\n'
end
end
def run
# Start the file handler. This handles its own printing.
initFileHandler
if Benben.args["render"].called?
runRendering
else
runNormal
end
end
end
end
###
### Entry point
###
begin
RemiLib.log = RemiLib::ConcurrentLogger.new
RemiLib.log.as(RemiLib::ConcurrentLogger).start
sleep 15.milliseconds # Because fibers don't immediately start *sigh*
Benben::Program.new.run
rescue err : RemiLib::Args::ArgumentError
RemiLib.log.fatal("#{err}")
rescue err : YAML::Error
RemiLib.log.fatal("Config error: #{err}", 2)
rescue err : Yuno::YunoError
{% if flag?(:benben_debug) %}
RemiLib.log.fatal(err)
{% else %}
RemiLib.log.fatal("Internal error: #{err}", 128)
{% end %}
rescue err : Benben::BenbenError
Benben.ui.try do |ui|
begin
ui.deinit
rescue Exception
end
end
RemiSlang.deinit
RemiLib.log.fatal("#{err}", 1)
ensure
RemiLib.log.as(RemiLib::ConcurrentLogger).stop
RemiLib.log.as(RemiLib::ConcurrentLogger).drain
Benben.logFile?.try do |file|
file.flush
file.close
end
end
###
### KLUDGE: Totally janky main() override since we can't spawn first-class
### threads in Crystal yet. We don't want to spawn too many worker threads
### because that just wastes system resources. We can't spawn too few or
### performance degrades (or the program gets glitchy). So we want to spawn a
### good amount.
###
### Ideally we could just have the option to spawn Threads ourselves instead of
### _only_ Fibers. Then we wouldn't need to pre-process the command line,
### override an environment variable, override main(), bind atoll...
###
### https://portal.mozz.us/gemini/nanako.mooo.com/gemlog/2023-11-01-a.gmi
###
lib LibC
fun atoll(str : UInt8*) : Int64
end
fun main(argc : Int32, argv : UInt8**) : Int32
rendering : Bool = false
jobsNext : Bool = false
jobs : Int64 = -1
minWorkers : Int32 = 5
# Attempt to detect --render/-n and --jobs.
argc.times do |idx|
str = String.new(argv[idx])
if str == "--render" || str == "-n"
rendering = true
elsif str == "--jobs"
jobsNext = true
elsif jobsNext
jobs = LibC.atoll(str) # Will return 0 if it can't be converted
jobsNext = false
end
end
if rendering
# We don't really care if jobs is 0 or negative or whatever. We just want
# to spawn a positive number of worker threads. The actual command line
# checking will handle invalid values later on.
if jobs > 0
# +2 so we don't starve ourselves of worker threads. We always want at
# least minWorkers, though.
LibC.setenv("CRYSTAL_WORKERS", Math.max((jobs + 2), minWorkers).to_s, 1)
else
# This will need to be raised in the future if Benben gets more heavily
# parallel. We should ideally always have a few spare workers for the
# extra fibers we spawn.
LibC.setenv("CRYSTAL_WORKERS", Math.max(System.cpu_count + 2, minWorkers).to_s, 1)
end
else
# This will need to be raised in the future if Benben gets more heavily
# parallel. We should ideally always have one spare worker.
LibC.setenv("CRYSTAL_WORKERS", minWorkers.to_s, 1)
end
Crystal.main(argc, argv)
end
|