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>
361 lines
11 KiB
C++
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;
|
|
}
|