// 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 #include #include #include #include #include #include #include #include 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 "": 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& out, uint32_t v) { for (int i = 0; i < 4; ++i) out.push_back((unsigned char)(v >> (8 * i))); } void put_u16(std::vector& 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& 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 data; put_u32(data, frames); uint32_t value_bits; std::memcpy(&value_bits, &kWavValue, 4); put_u32(data, value_bits); std::vector 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 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& out) { const unsigned char* p = static_cast(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 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 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 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; }