Artifact ac20fd4348cf6379105bbebad634c3552170b1cc4508d54708d2bd9857b698a3:

  • File src/main.cr — part of check-in [3c7ac9a929] at 2023-12-28 11:42:40 on branch crystal-version — Add the --quiet option (user: alexa size: 9731) [more...]

#### 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