2026-03-24 16:14:05 +08:00
|
|
|
#include <XCEngine/Resources/AudioClip/AudioLoader.h>
|
|
|
|
|
#include <XCEngine/Core/Asset/ResourceManager.h>
|
|
|
|
|
#include <XCEngine/Core/Asset/ResourceTypes.h>
|
2026-03-22 02:03:51 +08:00
|
|
|
#include <cstring>
|
2026-03-17 22:32:27 +08:00
|
|
|
|
|
|
|
|
namespace XCEngine {
|
|
|
|
|
namespace Resources {
|
|
|
|
|
|
2026-03-22 02:03:51 +08:00
|
|
|
namespace {
|
|
|
|
|
|
|
|
|
|
struct WAVHeader {
|
|
|
|
|
uint32_t sampleRate = 44100;
|
|
|
|
|
uint32_t channels = 2;
|
|
|
|
|
uint32_t bitsPerSample = 16;
|
|
|
|
|
uint32_t dataSize = 0;
|
2026-04-19 00:26:28 +08:00
|
|
|
uint32_t dataOffset = 0;
|
2026-03-22 02:03:51 +08:00
|
|
|
};
|
|
|
|
|
|
2026-04-19 00:26:28 +08:00
|
|
|
uint16_t ReadLE16(const uint8_t* data) {
|
|
|
|
|
return static_cast<uint16_t>(data[0]) |
|
|
|
|
|
(static_cast<uint16_t>(data[1]) << 8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint32_t ReadLE32(const uint8_t* data) {
|
|
|
|
|
return static_cast<uint32_t>(data[0]) |
|
|
|
|
|
(static_cast<uint32_t>(data[1]) << 8) |
|
|
|
|
|
(static_cast<uint32_t>(data[2]) << 16) |
|
|
|
|
|
(static_cast<uint32_t>(data[3]) << 24);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 02:03:51 +08:00
|
|
|
bool ParseWAVHeader(const uint8_t* data, size_t size, WAVHeader& header) {
|
2026-04-19 00:26:28 +08:00
|
|
|
if (size < 12) {
|
2026-03-22 02:03:51 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:26:28 +08:00
|
|
|
if (std::memcmp(data, "RIFF", 4) != 0) {
|
2026-03-22 02:03:51 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:26:28 +08:00
|
|
|
if (std::memcmp(data + 8, "WAVE", 4) != 0) {
|
2026-03-22 02:03:51 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:26:28 +08:00
|
|
|
bool foundFormatChunk = false;
|
|
|
|
|
bool foundDataChunk = false;
|
|
|
|
|
size_t offset = 12;
|
|
|
|
|
|
|
|
|
|
while (offset + 8 <= size) {
|
|
|
|
|
const uint8_t* chunk = data + offset;
|
|
|
|
|
const uint32_t chunkSize = ReadLE32(chunk + 4);
|
|
|
|
|
const size_t chunkDataOffset = offset + 8;
|
|
|
|
|
|
|
|
|
|
if (chunkDataOffset > size || chunkSize > size - chunkDataOffset) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (std::memcmp(chunk, "fmt ", 4) == 0) {
|
|
|
|
|
if (chunkSize < 16) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uint16_t audioFormat = ReadLE16(chunk + 8);
|
|
|
|
|
if (audioFormat != 1) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
header.channels = ReadLE16(chunk + 10);
|
|
|
|
|
header.sampleRate = ReadLE32(chunk + 12);
|
|
|
|
|
header.bitsPerSample = ReadLE16(chunk + 22);
|
|
|
|
|
foundFormatChunk = true;
|
|
|
|
|
} else if (std::memcmp(chunk, "data", 4) == 0) {
|
|
|
|
|
header.dataOffset = static_cast<uint32_t>(chunkDataOffset);
|
|
|
|
|
header.dataSize = chunkSize;
|
|
|
|
|
foundDataChunk = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
offset = chunkDataOffset + chunkSize + (chunkSize & 1u);
|
2026-03-22 02:03:51 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:26:28 +08:00
|
|
|
if (!foundFormatChunk || !foundDataChunk) {
|
2026-03-22 02:03:51 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:26:28 +08:00
|
|
|
if (header.channels == 0 || header.sampleRate == 0 || header.bitsPerSample == 0) {
|
2026-03-22 02:03:51 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:26:28 +08:00
|
|
|
if ((header.bitsPerSample % 8u) != 0u) {
|
2026-03-22 02:03:51 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:26:28 +08:00
|
|
|
const uint32_t bytesPerFrame =
|
|
|
|
|
static_cast<uint32_t>(header.channels) * (header.bitsPerSample / 8u);
|
|
|
|
|
if (bytesPerFrame == 0 || (header.dataSize % bytesPerFrame) != 0u) {
|
2026-03-22 02:03:51 +08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 00:26:28 +08:00
|
|
|
return header.dataOffset + header.dataSize <= size;
|
2026-03-22 02:03:51 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2026-03-17 22:32:27 +08:00
|
|
|
AudioLoader::AudioLoader() = default;
|
|
|
|
|
|
|
|
|
|
AudioLoader::~AudioLoader() = default;
|
|
|
|
|
|
|
|
|
|
Containers::Array<Containers::String> AudioLoader::GetSupportedExtensions() const {
|
|
|
|
|
Containers::Array<Containers::String> extensions;
|
|
|
|
|
extensions.PushBack("wav");
|
|
|
|
|
return extensions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AudioLoader::CanLoad(const Containers::String& path) const {
|
|
|
|
|
Containers::String ext = GetExtension(path);
|
2026-04-19 00:26:28 +08:00
|
|
|
return ext == "wav";
|
2026-03-17 22:32:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LoadResult AudioLoader::Load(const Containers::String& path, const ImportSettings* settings) {
|
|
|
|
|
Containers::Array<Core::uint8> data = ReadFileData(path);
|
|
|
|
|
if (data.Empty()) {
|
|
|
|
|
return LoadResult("Failed to read audio file: " + path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AudioClip* audioClip = new AudioClip();
|
|
|
|
|
audioClip->m_path = path;
|
|
|
|
|
audioClip->m_name = path;
|
|
|
|
|
audioClip->m_guid = ResourceGUID::Generate(path);
|
|
|
|
|
|
|
|
|
|
AudioFormat format = DetectAudioFormat(path, data);
|
2026-04-19 00:26:28 +08:00
|
|
|
if (format != AudioFormat::WAV) {
|
|
|
|
|
delete audioClip;
|
|
|
|
|
return LoadResult("Unsupported audio format: " + path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!ParseWAVData(data, audioClip)) {
|
|
|
|
|
delete audioClip;
|
|
|
|
|
return LoadResult("Failed to parse WAV data");
|
2026-03-22 02:03:51 +08:00
|
|
|
}
|
2026-04-19 00:26:28 +08:00
|
|
|
|
2026-03-17 22:32:27 +08:00
|
|
|
audioClip->SetAudioFormat(format);
|
|
|
|
|
|
|
|
|
|
audioClip->m_isValid = true;
|
|
|
|
|
audioClip->m_memorySize = sizeof(AudioClip) + audioClip->m_name.Length() +
|
2026-04-19 00:26:28 +08:00
|
|
|
audioClip->m_path.Length() + audioClip->GetPCMData().Size();
|
2026-03-17 22:32:27 +08:00
|
|
|
|
|
|
|
|
return LoadResult(audioClip);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImportSettings* AudioLoader::GetDefaultSettings() const {
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AudioLoader::ParseWAVData(const Containers::Array<Core::uint8>& data, AudioClip* audioClip) {
|
2026-04-19 00:26:28 +08:00
|
|
|
if (audioClip == nullptr) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 02:03:51 +08:00
|
|
|
WAVHeader header;
|
|
|
|
|
if (!ParseWAVHeader(data.Data(), data.Size(), header)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
audioClip->SetSampleRate(header.sampleRate);
|
|
|
|
|
audioClip->SetChannels(header.channels);
|
|
|
|
|
audioClip->SetBitsPerSample(header.bitsPerSample);
|
|
|
|
|
|
2026-04-19 00:26:28 +08:00
|
|
|
Containers::Array<Core::uint8> pcmData;
|
|
|
|
|
pcmData.ResizeUninitialized(header.dataSize);
|
|
|
|
|
if (header.dataSize > 0) {
|
|
|
|
|
std::memcpy(
|
|
|
|
|
pcmData.Data(),
|
|
|
|
|
data.Data() + header.dataOffset,
|
|
|
|
|
header.dataSize);
|
|
|
|
|
}
|
|
|
|
|
audioClip->SetPCMData(pcmData);
|
2026-03-22 02:03:51 +08:00
|
|
|
|
2026-03-17 22:32:27 +08:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AudioFormat AudioLoader::DetectAudioFormat(const Containers::String& path, const Containers::Array<Core::uint8>& data) {
|
|
|
|
|
Containers::String ext = GetExtension(path);
|
|
|
|
|
|
|
|
|
|
if (ext == "wav") return AudioFormat::WAV;
|
|
|
|
|
if (ext == "ogg") return AudioFormat::OGG;
|
|
|
|
|
if (ext == "mp3") return AudioFormat::MP3;
|
|
|
|
|
if (ext == "flac") return AudioFormat::FLAC;
|
|
|
|
|
|
|
|
|
|
if (data.Size() >= 4) {
|
|
|
|
|
if (data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F') {
|
|
|
|
|
return AudioFormat::WAV;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return AudioFormat::Unknown;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace Resources
|
|
|
|
|
} // namespace XCEngine
|