Torque3D/Engine/lib/openal-soft/alc/backends/wave.cpp

409 lines
12 KiB
C++
Raw Permalink Normal View History

/**
* OpenAL cross platform audio library
* Copyright (C) 1999-2007 by authors.
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
* Or go to http://www.gnu.org/copyleft/lgpl.html
*/
#include "config.h"
#include "wave.h"
#include <algorithm>
#include <atomic>
#include <cerrno>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <exception>
2024-06-30 19:35:57 +00:00
#include <system_error>
#include <thread>
2024-06-30 19:35:57 +00:00
#include <vector>
#include "albit.h"
#include "alc/alconfig.h"
#include "almalloc.h"
#include "alnumeric.h"
2024-06-30 19:35:57 +00:00
#include "althrd_setname.h"
#include "core/device.h"
#include "core/logging.h"
#include "strutils.h"
namespace {
2024-06-30 19:35:57 +00:00
using namespace std::string_view_literals;
using std::chrono::seconds;
using std::chrono::milliseconds;
using std::chrono::nanoseconds;
using ubyte = unsigned char;
using ushort = unsigned short;
2024-06-30 19:35:57 +00:00
struct FileDeleter {
void operator()(gsl::owner<FILE*> f) { fclose(f); }
};
using FilePtr = std::unique_ptr<FILE,FileDeleter>;
[[nodiscard]] constexpr auto GetDeviceName() noexcept { return "Wave File Writer"sv; }
2024-06-30 19:35:57 +00:00
constexpr std::array<ubyte,16> SUBTYPE_PCM{{
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa,
0x00, 0x38, 0x9b, 0x71
2024-06-30 19:35:57 +00:00
}};
constexpr std::array<ubyte,16> SUBTYPE_FLOAT{{
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa,
0x00, 0x38, 0x9b, 0x71
2024-06-30 19:35:57 +00:00
}};
2024-06-30 19:35:57 +00:00
constexpr std::array<ubyte,16> SUBTYPE_BFORMAT_PCM{{
0x01, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1,
0xca, 0x00, 0x00, 0x00
2024-06-30 19:35:57 +00:00
}};
2024-06-30 19:35:57 +00:00
constexpr std::array<ubyte,16> SUBTYPE_BFORMAT_FLOAT{{
0x03, 0x00, 0x00, 0x00, 0x21, 0x07, 0xd3, 0x11, 0x86, 0x44, 0xc8, 0xc1,
0xca, 0x00, 0x00, 0x00
2024-06-30 19:35:57 +00:00
}};
void fwrite16le(ushort val, FILE *f)
{
2024-06-30 19:35:57 +00:00
std::array data{static_cast<ubyte>(val&0xff), static_cast<ubyte>((val>>8)&0xff)};
fwrite(data.data(), 1, data.size(), f);
}
void fwrite32le(uint val, FILE *f)
{
2024-06-30 19:35:57 +00:00
std::array data{static_cast<ubyte>(val&0xff), static_cast<ubyte>((val>>8)&0xff),
static_cast<ubyte>((val>>16)&0xff), static_cast<ubyte>((val>>24)&0xff)};
fwrite(data.data(), 1, data.size(), f);
}
struct WaveBackend final : public BackendBase {
explicit WaveBackend(DeviceBase *device) noexcept : BackendBase{device} { }
~WaveBackend() override;
int mixerProc();
2024-06-30 19:35:57 +00:00
void open(std::string_view name) override;
bool reset() override;
void start() override;
void stop() override;
2024-06-30 19:35:57 +00:00
FilePtr mFile{nullptr};
long mDataStart{-1};
2024-06-30 19:35:57 +00:00
std::vector<std::byte> mBuffer;
std::atomic<bool> mKillNow{true};
std::thread mThread;
};
2024-06-30 19:35:57 +00:00
WaveBackend::~WaveBackend() = default;
int WaveBackend::mixerProc()
{
const milliseconds restTime{mDevice->mUpdateSize*1000/mDevice->mSampleRate / 2};
2024-06-30 19:35:57 +00:00
althrd_setname(GetMixerThreadName());
const size_t frameStep{mDevice->channelsFromFmt()};
const size_t frameSize{mDevice->frameSizeFromFmt()};
int64_t done{0};
auto start = std::chrono::steady_clock::now();
while(!mKillNow.load(std::memory_order_acquire)
&& mDevice->Connected.load(std::memory_order_acquire))
{
auto now = std::chrono::steady_clock::now();
/* This converts from nanoseconds to nanosamples, then to samples. */
const auto avail = int64_t{std::chrono::duration_cast<seconds>((now-start) *
mDevice->mSampleRate).count()};
if(avail-done < mDevice->mUpdateSize)
{
std::this_thread::sleep_for(restTime);
continue;
}
while(avail-done >= mDevice->mUpdateSize)
{
mDevice->renderSamples(mBuffer.data(), mDevice->mUpdateSize, frameStep);
done += mDevice->mUpdateSize;
if(al::endian::native != al::endian::little)
{
const uint bytesize{mDevice->bytesFromFmt()};
if(bytesize == 2)
{
2024-06-30 19:35:57 +00:00
const size_t len{mBuffer.size() & ~1_uz};
for(size_t i{0};i < len;i+=2)
std::swap(mBuffer[i], mBuffer[i+1]);
}
else if(bytesize == 4)
{
2024-06-30 19:35:57 +00:00
const size_t len{mBuffer.size() & ~3_uz};
for(size_t i{0};i < len;i+=4)
{
std::swap(mBuffer[i ], mBuffer[i+3]);
std::swap(mBuffer[i+1], mBuffer[i+2]);
}
}
}
const size_t fs{fwrite(mBuffer.data(), frameSize, mDevice->mUpdateSize, mFile.get())};
if(fs < mDevice->mUpdateSize || ferror(mFile.get()))
{
ERR("Error writing to file");
mDevice->handleDisconnect("Failed to write playback samples");
break;
}
}
/* For every completed second, increment the start time and reduce the
* samples done. This prevents the difference between the start time
* and current time from growing too large, while maintaining the
* correct number of samples to render.
*/
if(done >= mDevice->mSampleRate)
{
seconds s{done/mDevice->mSampleRate};
done %= mDevice->mSampleRate;
start += s;
}
}
return 0;
}
2024-06-30 19:35:57 +00:00
void WaveBackend::open(std::string_view name)
{
2024-06-30 19:35:57 +00:00
auto fname = ConfigValueStr({}, "wave", "file");
if(!fname) throw al::backend_exception{al::backend_error::NoDevice,
"No wave output filename"};
2024-06-30 19:35:57 +00:00
if(name.empty())
name = GetDeviceName();
else if(name != GetDeviceName())
throw al::backend_exception{al::backend_error::NoDevice, "Device name \"{}\" not found",
name};
/* There's only one "device", so if it's already open, we're done. */
if(mFile) return;
#ifdef _WIN32
{
2024-06-30 19:35:57 +00:00
std::wstring wname{utf8_to_wstr(fname.value())};
mFile = FilePtr{_wfopen(wname.c_str(), L"wb")};
}
#else
2024-06-30 19:35:57 +00:00
mFile = FilePtr{fopen(fname->c_str(), "wb")};
#endif
if(!mFile)
throw al::backend_exception{al::backend_error::DeviceError, "Could not open file '{}': {}",
*fname, std::generic_category().message(errno)};
mDeviceName = name;
}
bool WaveBackend::reset()
{
2024-06-30 19:35:57 +00:00
if(GetConfigValueBool({}, "wave", "bformat", false))
{
mDevice->FmtChans = DevFmtAmbi3D;
mDevice->mAmbiOrder = 1;
}
switch(mDevice->FmtType)
{
case DevFmtByte:
mDevice->FmtType = DevFmtUByte;
break;
case DevFmtUShort:
mDevice->FmtType = DevFmtShort;
break;
case DevFmtUInt:
mDevice->FmtType = DevFmtInt;
break;
case DevFmtUByte:
case DevFmtShort:
case DevFmtInt:
case DevFmtFloat:
break;
}
auto chanmask = 0u;
auto isbformat = false;
switch(mDevice->FmtChans)
{
case DevFmtMono: chanmask = 0x04; break;
case DevFmtStereo: chanmask = 0x01 | 0x02; break;
case DevFmtQuad: chanmask = 0x01 | 0x02 | 0x10 | 0x20; break;
case DevFmtX51: chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x200 | 0x400; break;
case DevFmtX61: chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x100 | 0x200 | 0x400; break;
case DevFmtX71: chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x010 | 0x020 | 0x200 | 0x400; break;
2024-06-30 19:35:57 +00:00
case DevFmtX7144:
mDevice->FmtChans = DevFmtX714;
[[fallthrough]];
case DevFmtX714:
chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x010 | 0x020 | 0x200 | 0x400 | 0x1000 | 0x4000
| 0x8000 | 0x20000;
break;
/* NOTE: Same as 7.1. */
case DevFmtX3D71: chanmask = 0x01 | 0x02 | 0x04 | 0x08 | 0x010 | 0x020 | 0x200 | 0x400; break;
case DevFmtAmbi3D:
/* .amb output requires FuMa */
2024-06-30 19:35:57 +00:00
mDevice->mAmbiOrder = std::min(mDevice->mAmbiOrder, 3u);
mDevice->mAmbiLayout = DevAmbiLayout::FuMa;
mDevice->mAmbiScale = DevAmbiScaling::FuMa;
isbformat = true;
chanmask = 0;
break;
}
const auto bytes = mDevice->bytesFromFmt();
const auto channels = mDevice->channelsFromFmt();
if(fseek(mFile.get(), 0, SEEK_CUR) != 0)
{
/* ESPIPE means the underlying file isn't seekable, which is fine for
* piped output.
*/
if(auto errcode = errno; errcode != ESPIPE)
{
ERR("Failed to reset file offset: {} ({})", std::generic_category().message(errcode),
errcode);
}
}
clearerr(mFile.get());
2024-06-30 19:35:57 +00:00
fputs("RIFF", mFile.get());
fwrite32le(0xFFFFFFFF, mFile.get()); // 'RIFF' header len; filled in at stop
2024-06-30 19:35:57 +00:00
fputs("WAVE", mFile.get());
2024-06-30 19:35:57 +00:00
fputs("fmt ", mFile.get());
fwrite32le(40, mFile.get()); // 'fmt ' header len; 40 bytes for EXTENSIBLE
// 16-bit val, format type id (extensible: 0xFFFE)
2024-06-30 19:35:57 +00:00
fwrite16le(0xFFFE, mFile.get());
// 16-bit val, channel count
2024-06-30 19:35:57 +00:00
fwrite16le(static_cast<ushort>(channels), mFile.get());
// 32-bit val, frequency
fwrite32le(mDevice->mSampleRate, mFile.get());
// 32-bit val, bytes per second
fwrite32le(mDevice->mSampleRate * channels * bytes, mFile.get());
// 16-bit val, frame size
2024-06-30 19:35:57 +00:00
fwrite16le(static_cast<ushort>(channels * bytes), mFile.get());
// 16-bit val, bits per sample
2024-06-30 19:35:57 +00:00
fwrite16le(static_cast<ushort>(bytes * 8), mFile.get());
// 16-bit val, extra byte count
2024-06-30 19:35:57 +00:00
fwrite16le(22, mFile.get());
// 16-bit val, valid bits per sample
2024-06-30 19:35:57 +00:00
fwrite16le(static_cast<ushort>(bytes * 8), mFile.get());
// 32-bit val, channel mask
2024-06-30 19:35:57 +00:00
fwrite32le(chanmask, mFile.get());
// 16 byte GUID, sub-type format
2024-06-30 19:35:57 +00:00
std::ignore = fwrite((mDevice->FmtType == DevFmtFloat) ?
(isbformat ? SUBTYPE_BFORMAT_FLOAT.data() : SUBTYPE_FLOAT.data()) :
(isbformat ? SUBTYPE_BFORMAT_PCM.data() : SUBTYPE_PCM.data()), 1, 16, mFile.get());
2024-06-30 19:35:57 +00:00
fputs("data", mFile.get());
fwrite32le(0xFFFFFFFF, mFile.get()); // 'data' header len; filled in at stop
2024-06-30 19:35:57 +00:00
if(ferror(mFile.get()))
{
ERR("Error writing header: {}", std::generic_category().message(errno));
return false;
}
2024-06-30 19:35:57 +00:00
mDataStart = ftell(mFile.get());
setDefaultWFXChannelOrder();
const uint bufsize{mDevice->frameSizeFromFmt() * mDevice->mUpdateSize};
mBuffer.resize(bufsize);
return true;
}
void WaveBackend::start()
{
2024-06-30 19:35:57 +00:00
if(mDataStart > 0 && fseek(mFile.get(), 0, SEEK_END) != 0)
WARN("Failed to seek on output file");
try {
mKillNow.store(false, std::memory_order_release);
mThread = std::thread{&WaveBackend::mixerProc, this};
}
catch(std::exception& e) {
throw al::backend_exception{al::backend_error::DeviceError,
"Failed to start mixing thread: {}", e.what()};
}
}
void WaveBackend::stop()
{
if(mKillNow.exchange(true, std::memory_order_acq_rel) || !mThread.joinable())
return;
mThread.join();
if(mDataStart > 0)
{
2024-06-30 19:35:57 +00:00
long size{ftell(mFile.get())};
if(size > 0)
{
long dataLen{size - mDataStart};
2024-06-30 19:35:57 +00:00
if(fseek(mFile.get(), 4, SEEK_SET) == 0)
fwrite32le(static_cast<uint>(size-8), mFile.get()); // 'WAVE' header len
if(fseek(mFile.get(), mDataStart-4, SEEK_SET) == 0)
fwrite32le(static_cast<uint>(dataLen), mFile.get()); // 'data' header len
}
}
}
} // namespace
bool WaveBackendFactory::init()
{ return true; }
bool WaveBackendFactory::querySupport(BackendType type)
{ return type == BackendType::Playback; }
2024-06-30 19:35:57 +00:00
auto WaveBackendFactory::enumerate(BackendType type) -> std::vector<std::string>
{
switch(type)
{
case BackendType::Playback:
2024-06-30 19:35:57 +00:00
return std::vector{std::string{GetDeviceName()}};
case BackendType::Capture:
break;
}
2024-06-30 19:35:57 +00:00
return {};
}
BackendPtr WaveBackendFactory::createBackend(DeviceBase *device, BackendType type)
{
if(type == BackendType::Playback)
return BackendPtr{new WaveBackend{device}};
return nullptr;
}
BackendFactory &WaveBackendFactory::getFactory()
{
static WaveBackendFactory factory{};
return factory;
}