MilesLib: run the 40250 sound sources verbatim over an AIL software mixer
Copy SoundBase/SoundData/SoundInstance*/SoundManager*/Type from 40250 and implement the AIL_* calls they make in MilesAil.cpp (2D pan, 3D distance and listener pan, threaded stream decode, loop counts). The SDL host drains the mixer. CSoundManager is created/updated/destroyed where 40250 does it and the old AudioCommands bridge is removed. Port-map MilesLib 142/142 PORTED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
d509f0a8b4
commit
11991d4d1f
@@ -11,7 +11,6 @@
|
||||
#include "ui_scale.h"
|
||||
#include "vulkan_window.h"
|
||||
|
||||
#include "platform/MilesLib/AudioCommands.h"
|
||||
#include "platform/PackBackend.h"
|
||||
#include "platform/ScriptLib/PythonBoot.h"
|
||||
#include "platform/UserInterface/ServerClock.h"
|
||||
|
||||
+39
-196
@@ -2,232 +2,75 @@
|
||||
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
#include "audio_decode.h"
|
||||
#include "texture_decode.h"
|
||||
#include "platform/MilesLib/AudioCommands.h"
|
||||
#include "platform/MilesLib/MilesMixer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mt_host {
|
||||
|
||||
namespace {
|
||||
|
||||
bool decode_miles_file(const void* data, std::size_t size, std::vector<float>& out) {
|
||||
if (!decode_audio(static_cast<const std::uint8_t*>(data), size, out)) {
|
||||
SDL_Log("audio: cannot decode a %zu-byte sound file", size);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static_assert(MT_MILES_MIX_RATE == kMixRate && kMixChannels == 2, "Miles mix format");
|
||||
|
||||
NativeAudioEngine::NativeAudioEngine() {
|
||||
if (SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
||||
SDL_AudioSpec spec{};
|
||||
spec.format = SDL_AUDIO_F32;
|
||||
spec.channels = 2;
|
||||
spec.freq = 44100;
|
||||
spec.channels = kMixChannels;
|
||||
spec.freq = kMixRate;
|
||||
stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
|
||||
if (stream_)
|
||||
SDL_ResumeAudioStreamDevice(stream_);
|
||||
else
|
||||
SDL_Log("audio: no playback device: %s", SDL_GetError());
|
||||
}
|
||||
// Always DrainAudioCommands() from cold boot so stale queued commands don't accumulate.
|
||||
(void)DrainAudioCommands();
|
||||
// Without a device the Miles driver does not open, like 40250 with no sound card.
|
||||
if (stream_)
|
||||
MtMilesSetDecoder(decode_miles_file);
|
||||
const char* trace = std::getenv("MT_AUDIO_TRACE");
|
||||
trace_ = trace && *trace == '1';
|
||||
}
|
||||
|
||||
NativeAudioEngine::~NativeAudioEngine() {
|
||||
MtMilesSetDecoder(nullptr);
|
||||
if (stream_)
|
||||
SDL_DestroyAudioStream(stream_);
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
}
|
||||
|
||||
void NativeAudioEngine::pump() {
|
||||
const auto commands = DrainAudioCommands();
|
||||
for (const auto& cmd : commands) {
|
||||
++commands_drained_;
|
||||
switch (cmd.type) {
|
||||
case AudioCommand::PlaySound2D: {
|
||||
const auto* clip = get_clip(cmd.filename);
|
||||
if (clip && !clip->empty()) {
|
||||
Voice v{};
|
||||
v.samples = clip;
|
||||
v.volume = sound_volume_;
|
||||
v.target_volume = sound_volume_;
|
||||
v.filename = cmd.filename;
|
||||
voices_.push_back(std::move(v));
|
||||
++played_2d_;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AudioCommand::PlaySound3D: {
|
||||
const auto* clip = get_clip(cmd.filename);
|
||||
if (clip && !clip->empty()) {
|
||||
Voice v{};
|
||||
v.samples = clip;
|
||||
v.volume = sound_volume_ * std::clamp(cmd.volume, 0.0f, 1.0f);
|
||||
v.target_volume = v.volume;
|
||||
v.loop = cmd.play_count != 1;
|
||||
v.is_3d = true;
|
||||
v.handle = cmd.id;
|
||||
v.filename = cmd.filename;
|
||||
voices_.push_back(std::move(v));
|
||||
++played_3d_;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case AudioCommand::StopSound3D:
|
||||
voices_.erase(
|
||||
std::remove_if(voices_.begin(), voices_.end(), [&](const Voice& v) {
|
||||
return v.is_3d && v.handle == cmd.id;
|
||||
}),
|
||||
voices_.end());
|
||||
break;
|
||||
case AudioCommand::StopAllSound3D:
|
||||
voices_.erase(
|
||||
std::remove_if(voices_.begin(), voices_.end(), [](const Voice& v) { return v.is_3d; }),
|
||||
voices_.end());
|
||||
break;
|
||||
case AudioCommand::SetSoundVolume3D:
|
||||
for (auto& v : voices_) {
|
||||
if (v.is_3d && v.handle == cmd.id) {
|
||||
v.volume = sound_volume_ * std::clamp(cmd.volume, 0.0f, 1.0f);
|
||||
v.target_volume = v.volume;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AudioCommand::PlayMusic:
|
||||
case AudioCommand::FadeInMusic: {
|
||||
std::vector<std::uint8_t> bytes;
|
||||
if (cmd.filename.empty() || !read_live_pack_texture(cmd.filename, bytes)) break;
|
||||
if (pending_music_.valid()) superseded_music_.push_back(std::move(pending_music_));
|
||||
// The pack is read here (CEterPackManager is not thread-safe); only the decode runs off-thread.
|
||||
pending_music_ = std::async(std::launch::async, [bytes = std::move(bytes), name = cmd.filename]() -> Clip {
|
||||
auto clip = std::make_shared<std::vector<float>>();
|
||||
if (!decode_audio(bytes.data(), bytes.size(), *clip))
|
||||
SDL_Log("audio: cannot decode %s (%zu bytes)", name.c_str(), bytes.size());
|
||||
return clip;
|
||||
});
|
||||
pending_music_file_ = cmd.filename;
|
||||
pending_music_fade_in_ = cmd.type == AudioCommand::FadeInMusic;
|
||||
pending_music_volume_ = cmd.volume;
|
||||
break;
|
||||
}
|
||||
case AudioCommand::FadeOutMusic:
|
||||
case AudioCommand::FadeOutAllMusic:
|
||||
pending_music_file_.clear(); // a track still decoding is dropped when it finishes
|
||||
if (bgm_.samples) {
|
||||
bgm_.target_volume = 0.0f;
|
||||
bgm_.fade_step_per_frame = -std::max(bgm_.volume, 0.01f) / (44100.0f * 1.0f);
|
||||
bgm_.stop_after_fade = true;
|
||||
}
|
||||
break;
|
||||
case AudioCommand::FadeLimitOutMusic:
|
||||
if (bgm_.samples) {
|
||||
bgm_.target_volume = music_volume_ * std::clamp(cmd.volume, 0.0f, 1.0f);
|
||||
bgm_.fade_step_per_frame = (bgm_.target_volume - bgm_.volume) / (44100.0f * 1.0f);
|
||||
bgm_.stop_after_fade = false;
|
||||
}
|
||||
break;
|
||||
case AudioCommand::SetMusicVolume:
|
||||
music_volume_ = std::clamp(cmd.volume, 0.0f, 1.0f);
|
||||
if (bgm_.samples && !bgm_.stop_after_fade) {
|
||||
bgm_.volume = music_volume_;
|
||||
bgm_.target_volume = music_volume_;
|
||||
}
|
||||
break;
|
||||
case AudioCommand::SetSoundVolume:
|
||||
sound_volume_ = std::clamp(cmd.volume, 0.0f, 1.0f);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (pending_music_.valid() &&
|
||||
pending_music_.wait_for(std::chrono::seconds(0)) == std::future_status::ready) {
|
||||
Clip clip = pending_music_.get();
|
||||
if (!pending_music_file_.empty() && clip && !clip->empty())
|
||||
start_music(std::move(clip), pending_music_file_, pending_music_fade_in_, pending_music_volume_);
|
||||
pending_music_file_.clear();
|
||||
}
|
||||
superseded_music_.erase(
|
||||
std::remove_if(superseded_music_.begin(), superseded_music_.end(), [](const std::future<Clip>& f) {
|
||||
return f.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
|
||||
}),
|
||||
superseded_music_.end());
|
||||
|
||||
if (!stream_) return;
|
||||
const int queued_bytes = SDL_GetAudioStreamAvailable(stream_);
|
||||
if (queued_bytes < 0) return;
|
||||
const std::size_t queued_frames = static_cast<std::size_t>(queued_bytes) / (2 * sizeof(float));
|
||||
constexpr std::size_t kTargetQueuedFrames = 4410; // ~100 ms stereo @ 44.1 kHz
|
||||
const std::size_t queued_frames = static_cast<std::size_t>(queued_bytes) / (kMixChannels * sizeof(float));
|
||||
constexpr std::size_t kTargetQueuedFrames = kMixRate / 10; // ~100 ms
|
||||
if (queued_frames >= kTargetQueuedFrames) return;
|
||||
const std::size_t frames_to_mix = kTargetQueuedFrames - queued_frames;
|
||||
mix_buffer_.assign(frames_to_mix * 2, 0.0f);
|
||||
|
||||
auto mix_voice = [&](Voice& v) -> bool {
|
||||
if (!v.samples || v.samples->empty()) return false;
|
||||
const std::size_t total_frames = v.samples->size() / 2;
|
||||
if (total_frames == 0) return false;
|
||||
const float* src = v.samples->data();
|
||||
for (std::size_t f = 0; f < frames_to_mix; ++f) {
|
||||
if (v.frame_cursor >= total_frames) {
|
||||
if (v.loop) v.frame_cursor = 0;
|
||||
else return false;
|
||||
}
|
||||
if (v.fade_step_per_frame != 0.0f) {
|
||||
v.volume += v.fade_step_per_frame;
|
||||
if ((v.fade_step_per_frame > 0.0f && v.volume >= v.target_volume) ||
|
||||
(v.fade_step_per_frame < 0.0f && v.volume <= v.target_volume)) {
|
||||
v.volume = v.target_volume;
|
||||
v.fade_step_per_frame = 0.0f;
|
||||
if (v.stop_after_fade && v.volume <= 0.0001f) {
|
||||
v.samples = nullptr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
mix_buffer_[f * 2 + 0] += src[v.frame_cursor * 2 + 0] * v.volume;
|
||||
mix_buffer_[f * 2 + 1] += src[v.frame_cursor * 2 + 1] * v.volume;
|
||||
++v.frame_cursor;
|
||||
}
|
||||
return v.loop || v.frame_cursor < total_frames;
|
||||
};
|
||||
|
||||
if (bgm_.samples) {
|
||||
if (!mix_voice(bgm_)) {
|
||||
bgm_.samples = nullptr;
|
||||
bgm_clip_.reset();
|
||||
}
|
||||
}
|
||||
for (auto it = voices_.begin(); it != voices_.end();) {
|
||||
if (!mix_voice(*it)) it = voices_.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
for (float& sample : mix_buffer_)
|
||||
mix_buffer_.assign(frames_to_mix * kMixChannels, 0.0f);
|
||||
MtMilesMix(mix_buffer_.data(), frames_to_mix);
|
||||
for (float& sample : mix_buffer_) {
|
||||
if (trace_) trace_peak_ = std::max(trace_peak_, std::fabs(sample));
|
||||
sample = std::clamp(sample, -1.0f, 1.0f);
|
||||
SDL_PutAudioStreamData(stream_, mix_buffer_.data(), static_cast<int>(mix_buffer_.size() * sizeof(float)));
|
||||
}
|
||||
|
||||
void NativeAudioEngine::start_music(Clip clip, const std::string& filename, bool fade_in, float volume) {
|
||||
bgm_clip_ = std::move(clip);
|
||||
bgm_.samples = bgm_clip_.get();
|
||||
bgm_.frame_cursor = 0;
|
||||
bgm_.loop = true;
|
||||
bgm_.filename = filename;
|
||||
bgm_.stop_after_fade = false;
|
||||
const float target = music_volume_ * std::clamp(volume, 0.0f, 1.0f);
|
||||
bgm_.target_volume = target;
|
||||
if (fade_in) {
|
||||
bgm_.volume = 0.0f;
|
||||
bgm_.fade_step_per_frame = target / (44100.0f * 1.5f);
|
||||
} else {
|
||||
bgm_.volume = target;
|
||||
bgm_.fade_step_per_frame = 0.0f;
|
||||
}
|
||||
++played_music_;
|
||||
SDL_Log("audio: music %s (%.0f s)", filename.c_str(), double(bgm_clip_->size() / 2) / kMixRate);
|
||||
}
|
||||
|
||||
const std::vector<float>* NativeAudioEngine::get_clip(const std::string& vpath) {
|
||||
if (vpath.empty()) return nullptr;
|
||||
auto [it, inserted] = clips_.try_emplace(vpath);
|
||||
if (!inserted) return &it->second;
|
||||
std::vector<std::uint8_t> bytes;
|
||||
if (read_live_pack_texture(vpath, bytes) && !decode_audio(bytes.data(), bytes.size(), it->second))
|
||||
SDL_Log("audio: cannot decode %s (%zu bytes)", vpath.c_str(), bytes.size());
|
||||
return &it->second;
|
||||
if (trace_ && (trace_frames_ += frames_to_mix) >= std::size_t(kMixRate) * 2) {
|
||||
SDL_Log("audio: peak %.3f", trace_peak_);
|
||||
trace_peak_ = 0.0f;
|
||||
trace_frames_ = 0;
|
||||
}
|
||||
SDL_PutAudioStreamData(stream_, mix_buffer_.data(), static_cast<int>(mix_buffer_.size() * sizeof(float)));
|
||||
}
|
||||
|
||||
} // namespace mt_host
|
||||
|
||||
+8
-42
@@ -3,63 +3,29 @@
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace mt_host {
|
||||
|
||||
// Drains the MilesLib AudioCommand queue and mixes 2D/3D sounds and BGM into an SDL audio stream.
|
||||
// The audio device behind the MilesLib port: installs the .wav/.mp3 decoder of the Miles emulation
|
||||
// (platform/MilesLib/MilesMixer.h) and feeds its mix into an SDL audio stream. Constructed before
|
||||
// CSoundManager::Create, which opens the Miles driver only when a decoder is installed.
|
||||
class NativeAudioEngine {
|
||||
struct Voice {
|
||||
const std::vector<float>* samples = nullptr;
|
||||
std::size_t frame_cursor = 0;
|
||||
float volume = 1.0f;
|
||||
float target_volume = 1.0f;
|
||||
float fade_step_per_frame = 0.0f;
|
||||
bool stop_after_fade = false;
|
||||
bool loop = false;
|
||||
bool is_3d = false;
|
||||
int handle = 0;
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
public:
|
||||
NativeAudioEngine();
|
||||
|
||||
~NativeAudioEngine();
|
||||
|
||||
// Tops the device queue up to ~100 ms with MtMilesMix.
|
||||
void pump();
|
||||
|
||||
private:
|
||||
using Clip = std::shared_ptr<const std::vector<float>>;
|
||||
|
||||
const std::vector<float>* get_clip(const std::string& vpath);
|
||||
void start_music(Clip clip, const std::string& filename, bool fade_in, float volume);
|
||||
|
||||
SDL_AudioStream* stream_ = nullptr;
|
||||
std::unordered_map<std::string, std::vector<float>> clips_;
|
||||
std::vector<Voice> voices_;
|
||||
Voice bgm_{};
|
||||
// BGM is not cached in clips_ (a 5-minute track is ~100 MB of float): only the playing track is kept, and
|
||||
// a new one decodes on a worker thread while the old one keeps playing.
|
||||
Clip bgm_clip_;
|
||||
std::future<Clip> pending_music_;
|
||||
std::vector<std::future<Clip>> superseded_music_; // a std::async future blocks in its destructor
|
||||
std::string pending_music_file_;
|
||||
bool pending_music_fade_in_ = false;
|
||||
float pending_music_volume_ = 1.0f;
|
||||
std::vector<float> mix_buffer_;
|
||||
float sound_volume_ = 0.8f;
|
||||
float music_volume_ = 0.5f;
|
||||
std::size_t commands_drained_ = 0;
|
||||
std::size_t played_2d_ = 0;
|
||||
std::size_t played_3d_ = 0;
|
||||
std::size_t played_music_ = 0;
|
||||
// MT_AUDIO_TRACE=1: logs the peak level of the mix every 2 s.
|
||||
bool trace_ = false;
|
||||
float trace_peak_ = 0.0f;
|
||||
std::size_t trace_frames_ = 0;
|
||||
};
|
||||
|
||||
} // namespace mt_host
|
||||
|
||||
@@ -252,6 +252,8 @@ bool VulkanWindow::poll(bool forward_to_live_client) {
|
||||
}
|
||||
#ifdef MT_NATIVE_HAS_LIVE_CLIENT
|
||||
if (forward_to_live_client) {
|
||||
if (event.type == SDL_EVENT_WINDOW_FOCUS_GAINED || event.type == SDL_EVENT_WINDOW_FOCUS_LOST)
|
||||
PythonBoot::UIActivate(event.type == SDL_EVENT_WINDOW_FOCUS_GAINED);
|
||||
if (!hardware_cursor_enabled_ &&
|
||||
(event.type == SDL_EVENT_WINDOW_MOUSE_ENTER || event.type == SDL_EVENT_WINDOW_FOCUS_GAINED)) {
|
||||
std::printf(">>> SDL MOUSE ENTER/FOCUS GAINED\n");
|
||||
|
||||
Reference in New Issue
Block a user