Files
mtgodot-poc/tests/port/port_miles_test.cpp
T
shenleiandClaude Opus 5.5 11991d4d1f 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>
2026-09-30 12:07:37 +09:00

361 lines
11 KiB
C++

// MilesLib port: the 40250 CSoundManager/CSoundManager2D/3D/Stream code over the Miles emulation in
// platform/MilesLib/MilesAil.cpp, observed through the mix. Sound files are written to a temporary
// directory and read by path (no pack callbacks); a fake decoder turns them into constant clips.
#include "MilesLib/Stdafx.h"
#include "MilesLib/SoundManager.h"
#include "MilesLib/Type.h"
#include "EterBase/Timer.h"
#include "../../src/platform/MilesLib/MilesMixer.h"
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <thread>
#include <vector>
#include <unistd.h>
static int g_failures = 0;
#define CHECK(cond) \
do { \
if (!(cond)) { \
std::fprintf(stderr, "%s:%d: CHECK(%s)\n", __FILE__, __LINE__, #cond); \
++g_failures; \
} \
} while (0)
#define CHECK_NEAR(a, b) CHECK(std::fabs(double(a) - double(b)) < 1e-3)
namespace
{
// A file is "<magic><frames:u32><value:f32>": RIFF..WAVE with a PCM fmt chunk, or ID3 for an mp3. The
// fake decoder returns `frames` stereo frames of `value`.
constexpr float kWavValue = 0.125f;
constexpr float kMp3Value = 0.25f;
void put_u32(std::vector<unsigned char>& out, uint32_t v)
{
for (int i = 0; i < 4; ++i)
out.push_back((unsigned char)(v >> (8 * i)));
}
void put_u16(std::vector<unsigned char>& out, uint16_t v)
{
out.push_back((unsigned char)v);
out.push_back((unsigned char)(v >> 8));
}
std::string write_file(const std::string& dir, const char* name, const std::vector<unsigned char>& bytes)
{
const std::string path = dir + "/" + name;
FILE* file = std::fopen(path.c_str(), "wb");
std::fwrite(bytes.data(), 1, bytes.size(), file);
std::fclose(file);
return path;
}
std::string write_wav(const std::string& dir, const char* name, uint32_t frames)
{
std::vector<unsigned char> data;
put_u32(data, frames);
uint32_t value_bits;
std::memcpy(&value_bits, &kWavValue, 4);
put_u32(data, value_bits);
std::vector<unsigned char> out = {'R', 'I', 'F', 'F'};
put_u32(out, uint32_t(4 + 8 + 16 + 8 + data.size()));
out.insert(out.end(), {'W', 'A', 'V', 'E', 'f', 'm', 't', ' '});
put_u32(out, 16);
put_u16(out, 1); // PCM
put_u16(out, 2);
put_u32(out, 44100);
put_u32(out, 44100 * 4);
put_u16(out, 4);
put_u16(out, 16);
out.insert(out.end(), {'d', 'a', 't', 'a'});
put_u32(out, uint32_t(data.size()));
out.insert(out.end(), data.begin(), data.end());
return write_file(dir, name, out);
}
std::string write_mp3(const std::string& dir, const char* name, uint32_t frames)
{
std::vector<unsigned char> out = {'I', 'D', '3'};
put_u32(out, frames);
return write_file(dir, name, out);
}
bool fake_decoder(const void* data, std::size_t size, std::vector<float>& out)
{
const unsigned char* p = static_cast<const unsigned char*>(data);
uint32_t frames;
float value;
if (size >= 7 && std::memcmp(p, "ID3", 3) == 0)
{
std::memcpy(&frames, p + 3, 4);
value = kMp3Value;
}
else if (size >= 52 && std::memcmp(p, "RIFF", 4) == 0)
{
std::memcpy(&frames, p + 44, 4);
std::memcpy(&value, p + 48, 4);
}
else
return false;
out.assign(std::size_t(frames) * 2, value);
return true;
}
struct Mix
{
float left = 0.0f;
float right = 0.0f;
};
// The first frame of a `frames`-frame mix.
Mix mix(std::size_t frames = 1)
{
std::vector<float> out(frames * 2, 0.0f);
MtMilesMix(out.data(), frames);
return {out[0], out[1]};
}
void volume_conversion(CSoundManager& manager)
{
manager.SetSoundVolumeRatio(0.05f); // below 0.1: as is
CHECK_NEAR(manager.GetSoundVolume(), 0.05f);
manager.SetSoundVolumeRatio(0.5f); // 10^(ratio - 1)
CHECK_NEAR(manager.GetSoundVolume(), std::pow(10.0f, -0.5f));
manager.SetSoundVolumeGrade(5);
CHECK_NEAR(manager.GetSoundVolume(), 1.0f);
manager.SetSoundVolume(2.0f);
CHECK_NEAR(manager.GetSoundVolume(), 1.0f);
}
void sound_2d(CSoundManager& manager, const std::string& wav)
{
manager.SetSoundVolume(1.0f);
// Four instances: a fifth sound while all play is dropped.
for (int i = 0; i < 5; ++i)
manager.PlaySound2D(wav.c_str());
Mix m = mix();
CHECK_NEAR(m.left, 4 * kWavValue);
CHECK_NEAR(m.right, 4 * kWavValue);
// One play of 100 frames: the rest of the first 150 is silent and the instances are done.
std::vector<float> out(150 * 2, 0.0f);
MtMilesMix(out.data(), 150);
CHECK_NEAR(out[98 * 2], 4 * kWavValue);
CHECK_NEAR(out[99 * 2], 0.0f);
CHECK_NEAR(mix().left, 0.0f);
manager.PlaySound2D(wav.c_str());
CHECK_NEAR(mix().left, kWavValue);
mix(200);
// Volume 0: nothing starts.
manager.SetSoundVolume(0.0f);
manager.PlaySound2D(wav.c_str());
CHECK_NEAR(mix().left, 0.0f);
manager.SetSoundVolume(1.0f);
}
void sound_3d(CSoundManager& manager, const std::string& wav, const std::string& mp3)
{
manager.SetPosition(1000.0f, 2000.0f, 0.0f);
// A right-handed camera looking down -z with +y up has +x on its right.
manager.SetDirection(0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f);
manager.Update();
// At the listener: full volume on both sides.
manager.PlaySound3D(1000.0f, 2000.0f, 0.0f, wav.c_str(), 1);
Mix m = mix();
CHECK_NEAR(m.left, kWavValue);
CHECK_NEAR(m.right, kWavValue);
manager.StopAllSound3D();
CHECK_NEAR(mix().left, 0.0f);
// 10 Miles units (x 200 sound scale) to the right: 1/10, right channel only.
manager.PlaySound3D(1000.0f + 2000.0f, 2000.0f, 0.0f, wav.c_str(), 1);
m = mix();
CHECK_NEAR(m.left, 0.0f);
CHECK_NEAR(m.right, 0.1f * kWavValue);
manager.StopAllSound3D();
// Beyond the maximum distance: silent.
manager.PlaySound3D(1000.0f - 200.0f * 250.0f, 2000.0f, 0.0f, wav.c_str(), 1);
m = mix();
CHECK_NEAR(m.left, 0.0f);
CHECK_NEAR(m.right, 0.0f);
manager.StopAllSound3D();
// An mp3 decompresses to a WAV image; loop count 0 plays forever.
manager.PlaySound3D(1000.0f, 2000.0f, 0.0f, mp3.c_str(), 0);
mix(1000);
CHECK_NEAR(mix().left, kMp3Value);
manager.StopAllSound3D();
CHECK_NEAR(mix().left, 0.0f);
// Loop count 2: two plays, then done.
manager.PlaySound3D(1000.0f, 2000.0f, 0.0f, wav.c_str(), 2);
std::vector<float> out(250 * 2, 0.0f);
MtMilesMix(out.data(), 250);
CHECK_NEAR(out[150 * 2], kWavValue);
CHECK_NEAR(out[199 * 2], kWavValue);
CHECK_NEAR(out[200 * 2], 0.0f);
manager.StopAllSound3D();
// Character sounds: one per file within 0.3 s, none beyond 5000 units.
CTimer::Instance().Advance();
manager.PlayCharacterSound3D(1000.0f, 2000.0f, 0.0f, wav.c_str(), TRUE);
manager.PlayCharacterSound3D(1000.0f, 2000.0f, 0.0f, wav.c_str(), TRUE);
CHECK_NEAR(mix().left, kWavValue);
manager.StopAllSound3D(); // also clears the history
manager.PlayCharacterSound3D(1000.0f, 2000.0f, 0.0f, wav.c_str(), FALSE);
manager.PlayCharacterSound3D(1000.0f, 2000.0f, 0.0f, wav.c_str(), FALSE);
CHECK_NEAR(mix().left, 2 * kWavValue);
manager.StopAllSound3D();
manager.PlayCharacterSound3D(1000.0f + 5001.0f, 2000.0f, 0.0f, wav.c_str(), TRUE);
CHECK_NEAR(mix().right, 0.0f);
CHECK(manager.PlayAmbienceSound3D(1000.0f, 2000.0f, 0.0f, wav.c_str(), 1) >= 0);
CHECK_NEAR(mix().left, kWavValue);
manager.StopAllSound3D();
// UpdateSoundInstance plays the entries of the current frame.
NSound::TSoundInstanceVector instances(2);
instances[0].dwFrame = 3;
instances[0].strSoundFileName = wav;
instances[1].dwFrame = 4;
instances[1].strSoundFileName = wav;
manager.UpdateSoundInstance(1000.0f, 2000.0f, 0.0f, 3, &instances, FALSE);
CHECK_NEAR(mix().left, kWavValue);
manager.StopAllSound3D();
}
// The stream decodes on a worker thread: mix until the level is `expected` (or give up after 1 s).
float wait_for_stream(float expected)
{
float level = 0.0f;
for (int i = 0; i < 200; ++i)
{
level = mix().left;
if (std::fabs(level - expected) < 1e-3f)
break;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
return level;
}
void music(CSoundManager& manager, const std::string& song, const std::string& other)
{
manager.SetMusicVolume(1.0f);
manager.FadeInMusic(song.c_str(), 0.25f);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
CHECK_NEAR(mix().left, 0.0f); // starts at volume 0
manager.Update();
CHECK_NEAR(wait_for_stream(0.25f * kMp3Value), 0.25f * kMp3Value);
for (int i = 0; i < 5; ++i)
manager.Update();
CHECK_NEAR(mix().left, kMp3Value); // clamped at the music volume
manager.SetMusicVolume(0.5f);
CHECK_NEAR(mix().left, 0.5f * kMp3Value);
// Down to a limit (a ratio, converted), then playing on.
manager.FadeLimitOutMusic(song.c_str(), 0.0f, 0.2f);
manager.Update();
CHECK_NEAR(mix().left, 0.3f * kMp3Value);
manager.Update();
manager.Update();
CHECK_NEAR(mix().left, 0.0f);
// A second song fades the first one out (0.01 per update) and fades in in the next slot.
manager.SetMusicVolume(1.0f);
CHECK_NEAR(mix().left, kMp3Value);
manager.FadeInMusic(other.c_str(), 0.5f);
manager.Update();
CHECK_NEAR(wait_for_stream((0.99f + 0.5f) * kMp3Value), (0.99f + 0.5f) * kMp3Value);
manager.Update();
CHECK_NEAR(mix().left, (0.98f + 1.0f) * kMp3Value);
// Fading out stops the stream at 0.
manager.FadeOutMusic(other.c_str(), 1.0f);
manager.Update();
CHECK_NEAR(mix().left, 0.97f * kMp3Value);
for (int i = 0; i < 100; ++i)
manager.Update();
CHECK_NEAR(mix().left, 0.0f);
// Save/RestoreVolume (window deactivated/activated).
manager.PlayMusic(song.c_str());
CHECK_NEAR(wait_for_stream(kMp3Value), kMp3Value);
manager.SaveVolume();
CHECK_NEAR(mix().left, 0.0f);
CHECK_NEAR(manager.GetSoundVolume(), 0.0f);
manager.SetSoundVolume(0.7f); // stored for the restore
manager.RestoreVolume();
CHECK_NEAR(mix().left, kMp3Value);
CHECK_NEAR(manager.GetSoundVolume(), 0.7f);
manager.FadeOutAllMusic();
for (int i = 0; i < 100; ++i)
manager.Update();
}
void sound_information()
{
NSound::TSoundDataVector data(2);
data[0].fTime = 0.5f;
data[0].strSoundFileName = "a.wav";
data[1].fTime = 0.1f;
data[1].strSoundFileName = "b.wav";
NSound::TSoundInstanceVector instances;
NSound::DataToInstance(data, &instances);
CHECK(instances.size() == 2);
// fTime / (1/60), truncated: 0.5 s is frame 29 in float.
CHECK(instances[0].dwFrame == 29 && instances[0].strSoundFileName == "a.wav");
CHECK(instances[1].dwFrame == 6 && instances[1].strSoundFileName == "b.wav");
}
} // namespace
int main()
{
char dir_template[] = "/tmp/port_miles_XXXXXX";
const char* dir = mkdtemp(dir_template);
if (!dir)
return 1;
const std::string wav = write_wav(dir, "hit.wav", 100);
const std::string mp3 = write_mp3(dir, "step.mp3", 400);
const std::string song = write_mp3(dir, "song.mp3", 44100);
const std::string other = write_mp3(dir, "other.mp3", 44100);
CTimer timer;
CSoundManager manager;
// No decoder means no audio device: the Miles driver does not open (40250 with no sound card).
CHECK(AIL_open_digital_driver(44100, 16, 2, 0) == NULL);
MtMilesSetDecoder(fake_decoder);
CHECK(manager.Create());
volume_conversion(manager);
sound_2d(manager, wav);
sound_3d(manager, wav, mp3);
music(manager, song, other);
sound_information();
manager.Destroy();
MtMilesSetDecoder(nullptr);
for (const char* name : {"hit.wav", "step.mp3", "song.mp3", "other.mp3"})
unlink((std::string(dir) + "/" + name).c_str());
rmdir(dir);
if (g_failures)
{
std::fprintf(stderr, "port_miles_test: %d failure(s)\n", g_failures);
return 1;
}
std::printf("port_miles_test: ok\n");
return 0;
}