diff --git a/.gitignore b/.gitignore index 796b96d..8a7e5ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /build +/.vscode +*.log diff --git a/include/ReMixer/sound.h b/include/ReMixer/sound.h index 1c4b2ae..8027db6 100644 --- a/include/ReMixer/sound.h +++ b/include/ReMixer/sound.h @@ -42,6 +42,13 @@ namespace ReMixer { /// @see LoadOGGVorbisFile(). static Sound FromOGGVorbisFile(const std::filesystem::path &file_name); + /// Returns a Sound object loaded from a WAV file, usually with a .wav or .wave extension. + /// The sample rate and channel mode will be read from the WAV header. + /// @param file_name The full path relative to the executable in which the file is located. + /// @see https://en.wikipedia.org/wiki/WAV + /// @see LoadWAVFile(). + static Sound FromWAVFile(const std::filesystem::path &file_name); + /// Returns a sound object loaded from a file containing raw PCM sound data in binary form. /// @param file_name The full path relative to the executable in which the file is located. /// @param sampleRate A uint that indicates the samples-per-second of the sound data. @@ -87,6 +94,12 @@ namespace ReMixer { /// @see FromOGGVorbisFile(). void LoadOGGVorbisFile(const std::filesystem::path &file_name); + /// Fills this sound instance with data from a file encoded with WAV, usually with a .wav or .wave extension. + /// @param file_name The full path relative to the executable in which the file is located. + /// @see https://en.wikipedia.org/wiki/WAV + /// @see FromWAVFile(). + void LoadWAVFile(const std::filesystem::path &file_name); + /// Fills this sound instance with data from a file containing raw PCM sound data. /// @param file_name The full path relative to the executable in which the file is located. /// @param sampleRate A uint that indicates the samples-per-second of the sound data. diff --git a/main.cpp b/main.cpp index e6fe03f..36bd7ec 100644 --- a/main.cpp +++ b/main.cpp @@ -39,7 +39,8 @@ //PulseStream test_stream2 = test.CreateStream("Another Test Stream"); //Sound test_sound = Sound::FromPCMFile("output.raw"); - Sound test_sound = Sound::FromOGGVorbisFile("wind.ogg"); + Sound test_sound = Sound::FromOGGVorbisFile("../test-sounds/wind.ogg"); + Sound wav_sound = Sound::FromWAVFile("../test-sounds/robodeath.wav"); Sound sine_sound = Sound::FromSineWave(5.f, 0.0025f, 3000, 0); Sound sawtooth_sound = Sound::FromSawtoothWave(5.f, 0.025f, 2000, 0); @@ -53,6 +54,7 @@ server.Play(test_stream, square_sound); server.Play(test_stream, noise_sound); server.Play(test_stream, test_sound); + server.Play(test_stream, wav_sound); while (true) { std::this_thread::sleep_for(25ms); diff --git a/src/linux/sound.cpp b/src/linux/sound.cpp index d9259dc..781dcab 100644 --- a/src/linux/sound.cpp +++ b/src/linux/sound.cpp @@ -3,6 +3,7 @@ #include #include #include +#include ReMixer::Sound ReMixer::Sound::FromOGGVorbisFile(const std::filesystem::path &file_name) { @@ -11,6 +12,12 @@ ReMixer::Sound ReMixer::Sound::FromOGGVorbisFile(const std::filesystem::path &fi return sound; } +ReMixer::Sound ReMixer::Sound::FromWAVFile(const std::filesystem::path &file_name) { + Sound sound; + sound.LoadWAVFile(file_name); + return sound; +} + ReMixer::Sound ReMixer::Sound::FromPCMFile(const std::filesystem::path &file_name, uint sample_rate, StreamMode stream_mode) { Sound sound; sound.LoadPCMFile(file_name, sample_rate, stream_mode); @@ -110,6 +117,94 @@ void ReMixer::Sound::LoadOGGVorbisFile(const std::filesystem::path &file_name) { } } +void ReMixer::Sound::LoadWAVFile(const std::filesystem::path &file_name) { + std::ifstream file(file_name, std::ios::binary); + if (!file.is_open()) { + throw std::runtime_error(std::format("Failed to open WAV file: {}", file_name.c_str())); + } + + auto read_u16 = [&](std::istream &s) -> uint16_t { + uint8_t bytes[2]; + s.read(reinterpret_cast(bytes), 2); + return bytes[0] | (bytes[1] << 8); + }; + + auto read_u32 = [&](std::istream &s) -> uint32_t { + uint8_t bytes[4]; + s.read(reinterpret_cast(bytes), 4); + return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24); + }; + + char riff[4]; + file.read(riff, 4); + if (strncmp(riff, "RIFF", 4) != 0) { + throw std::runtime_error("Invalid WAV file: missing RIFF header."); + } + + file.ignore(4); + + char wave[4]; + file.read(wave, 4); + if (strncmp(wave, "WAVE", 4) != 0) { + throw std::runtime_error("Invalid WAV file: missing WAVE header."); + } + + uint16_t num_channels = 0; + uint32_t sample_rate_ = 0; + uint16_t bits_per_sample = 0; + std::vector raw_data; + + while (!file.eof()) { + char chunk_id[4]; + file.read(chunk_id, 4); + if (file.eof()) break; + + uint32_t chunk_size = read_u32(file); + + if (strncmp(chunk_id, "fmt ", 4) == 0) { + uint16_t audio_format = read_u16(file); + num_channels = read_u16(file); + sample_rate_ = read_u32(file); + file.ignore(6); + bits_per_sample = read_u16(file); + + if (audio_format != 1 || bits_per_sample != 16) { + throw std::runtime_error("Unsupported WAV format: only PCM 16-bit is supported."); + } + + if (chunk_size > 16) { + file.ignore(chunk_size - 16); + } + } + else if (strncmp(chunk_id, "data", 4) == 0) { + raw_data.resize(chunk_size); + file.read(raw_data.data(), chunk_size); + break; + } + else { + file.ignore(chunk_size); + } + } + + if (raw_data.empty()) { + throw std::runtime_error("WAV file contains no audio data."); + } + + StreamMode mode; + if (num_channels == 1) { + mode = StreamMode::MONO; + } else if (num_channels == 2) { + mode = StreamMode::STEREO; + } else { + throw std::runtime_error(std::format("Unsupported WAV Channel count: {}", num_channels)); + } + + std::cout << std::format("Loaded WAV file: {} Hz, {} channel(s), {} bits\n", + sample_rate_, num_channels, bits_per_sample); + + LoadPCMBuffer(raw_data, sample_rate_, mode); +} + uint ReMixer::Sound::Channels() const { if (stream_mode == StreamMode::MONO) return 1; if (stream_mode == StreamMode::STEREO) return 2; diff --git a/test-sounds/robodeath.wav b/test-sounds/robodeath.wav new file mode 100644 index 0000000..1e1d702 Binary files /dev/null and b/test-sounds/robodeath.wav differ