Make Mockingboard voice actually play.
Signed-off-by: Andrea Odetti <mariofutire@gmail.com>
This commit is contained in:
parent
04ef0bf377
commit
5dee29fbf9
12 changed files with 361 additions and 19 deletions
|
@ -547,3 +547,12 @@ BYTE __stdcall SpkrToggle (WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecu
|
|||
|
||||
return MemReadFloatingBus(uExecutedCycles);
|
||||
}
|
||||
|
||||
// Mockingboard
|
||||
void registerSoundBuffer(IDirectSoundBuffer * buffer)
|
||||
{
|
||||
}
|
||||
|
||||
void unregisterSoundBuffer(IDirectSoundBuffer * buffer)
|
||||
{
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ add_executable(qapple
|
|||
audiogenerator.cpp
|
||||
loggingcategory.cpp
|
||||
viewbuffer.cpp
|
||||
qdirectsound.cpp
|
||||
|
||||
${QHEXVIEW_SOURCES}
|
||||
)
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
#include "emulator.h"
|
||||
#include "memorycontainer.h"
|
||||
#include "audiogenerator.h"
|
||||
#include "qdirectsound.h"
|
||||
#include "gamepadpaddle.h"
|
||||
#include "preferences.h"
|
||||
|
||||
|
@ -288,6 +289,7 @@ void QApple::on_stateChanged(QAudio::State state)
|
|||
void QApple::on_timer()
|
||||
{
|
||||
AudioGenerator::instance().start();
|
||||
QDirectSound::start();
|
||||
|
||||
if (!myElapsedTimer.isValid())
|
||||
{
|
||||
|
@ -304,6 +306,7 @@ void QApple::on_timer()
|
|||
|
||||
// just check if we got something to write
|
||||
AudioGenerator::instance().writeAudio();
|
||||
QDirectSound::writeAudio();
|
||||
|
||||
// wait next call
|
||||
return;
|
||||
|
@ -345,6 +348,7 @@ void QApple::on_timer()
|
|||
else
|
||||
{
|
||||
AudioGenerator::instance().writeAudio();
|
||||
QDirectSound::writeAudio();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -362,6 +366,7 @@ void QApple::restartTimeCounters()
|
|||
{
|
||||
// let them restart next time
|
||||
AudioGenerator::instance().stop();
|
||||
QDirectSound::stop();
|
||||
myElapsedTimer.invalidate();
|
||||
}
|
||||
|
||||
|
@ -465,6 +470,7 @@ void QApple::reloadOptions()
|
|||
|
||||
Paddle::instance() = GamepadPaddle::fromName(myOptions.gamepadName);
|
||||
AudioGenerator::instance().setOptions(myOptions.audioLatency, myOptions.silenceDelay, myOptions.volume);
|
||||
QDirectSound::setOptions(myOptions.audioLatency, myOptions.silenceDelay, myOptions.volume);
|
||||
}
|
||||
|
||||
void QApple::on_actionSave_state_triggered()
|
||||
|
|
|
@ -27,6 +27,7 @@ SOURCES += main.cpp\
|
|||
loggingcategory.cpp \
|
||||
options.cpp \
|
||||
qapple.cpp \
|
||||
qdirectsound.cpp \
|
||||
qresources.cpp \
|
||||
emulator.cpp \
|
||||
registry.cpp \
|
||||
|
@ -53,6 +54,7 @@ HEADERS += qapple.h \
|
|||
emulator.h \
|
||||
loggingcategory.h \
|
||||
options.h \
|
||||
qdirectsound.h \
|
||||
registry.h \
|
||||
video.h \
|
||||
memorycontainer.h \
|
||||
|
|
251
source/frontends/qapple/qdirectsound.cpp
Normal file
251
source/frontends/qapple/qdirectsound.cpp
Normal file
|
@ -0,0 +1,251 @@
|
|||
#include "qdirectsound.h"
|
||||
|
||||
#include "loggingcategory.h"
|
||||
#include "qdirectsound.h"
|
||||
#include "linux/windows/dsound.h"
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
|
||||
#include <QAudioOutput>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class DirectSoundGenerator
|
||||
{
|
||||
public:
|
||||
DirectSoundGenerator(IDirectSoundBuffer * buffer);
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
void writeAudio();
|
||||
void stateChanged(QAudio::State state);
|
||||
void setOptions(const qint32 initialSilence, const qint32 silenceDelay, const qint32 volume);
|
||||
|
||||
private:
|
||||
IDirectSoundBuffer * myBuffer;
|
||||
|
||||
typedef short int audio_t;
|
||||
|
||||
std::shared_ptr<QAudioOutput> myAudioOutput;
|
||||
QAudioFormat myAudioFormat;
|
||||
QIODevice * myDevice;
|
||||
|
||||
// options
|
||||
qint32 myInitialSilence;
|
||||
qint32 mySilenceDelay;
|
||||
|
||||
bool isRunning();
|
||||
void initialise();
|
||||
void writeEnoughSilence(const qint64 ms);
|
||||
};
|
||||
|
||||
|
||||
std::unordered_map<IDirectSoundBuffer *, std::shared_ptr<DirectSoundGenerator>> activeSoundGenerators;
|
||||
|
||||
DirectSoundGenerator::DirectSoundGenerator(IDirectSoundBuffer * buffer) : myBuffer(buffer)
|
||||
{
|
||||
myInitialSilence = 200;
|
||||
mySilenceDelay = 10000;
|
||||
}
|
||||
|
||||
void DirectSoundGenerator::initialise()
|
||||
{
|
||||
// only initialise here to skip all the buffers which are not in DSBSTATUS_PLAYING mode
|
||||
QAudioFormat audioFormat;
|
||||
audioFormat.setSampleRate(myBuffer->sampleRate);
|
||||
audioFormat.setChannelCount(myBuffer->channels);
|
||||
audioFormat.setSampleSize(myBuffer->bitsPerSample);
|
||||
audioFormat.setCodec(QString::fromUtf8("audio/pcm"));
|
||||
audioFormat.setByteOrder(QAudioFormat::LittleEndian);
|
||||
audioFormat.setSampleType(QAudioFormat::SignedInt);
|
||||
|
||||
myAudioOutput.reset(new QAudioOutput(audioFormat));
|
||||
myAudioFormat = myAudioOutput->format();
|
||||
}
|
||||
|
||||
void DirectSoundGenerator::setOptions(const qint32 initialSilence, const qint32 silenceDelay, const qint32 volume)
|
||||
{
|
||||
myInitialSilence = std::max(0, initialSilence);
|
||||
mySilenceDelay = std::max(0, silenceDelay);
|
||||
}
|
||||
|
||||
bool DirectSoundGenerator::isRunning()
|
||||
{
|
||||
if (!myAudioOutput)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const QAudio::State state = myAudioOutput->state();
|
||||
const QAudio::Error error = myAudioOutput->error();
|
||||
if (state == QAudio::ActiveState)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (state == QAudio::IdleState && error == QAudio::NoError)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DirectSoundGenerator::start()
|
||||
{
|
||||
if (isRunning())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DWORD dwStatus;
|
||||
myBuffer->GetStatus(&dwStatus);
|
||||
if (!(dwStatus & DSBSTATUS_PLAYING))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!myAudioOutput)
|
||||
{
|
||||
initialise();
|
||||
}
|
||||
|
||||
// restart as we are either starting or recovering from underrun
|
||||
myDevice = myAudioOutput->start();
|
||||
|
||||
if (!myDevice)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug(appleAudio) << "Restarting the AudioGenerator";
|
||||
|
||||
const int bytesSize = myAudioOutput->bufferSize();
|
||||
const qint32 frameSize = myAudioFormat.framesForBytes(bytesSize);
|
||||
|
||||
const qint32 framePeriod = myAudioFormat.framesForBytes(myAudioOutput->periodSize());
|
||||
qDebug(appleAudio) << "AudioOutput: size =" << frameSize << "f, period =" << framePeriod << "f";
|
||||
writeEnoughSilence(myInitialSilence); // ms
|
||||
}
|
||||
|
||||
void DirectSoundGenerator::stop()
|
||||
{
|
||||
if (!isRunning())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const qint32 bytesFree = myAudioOutput->bytesFree();
|
||||
|
||||
// fill with zeros and stop
|
||||
std::vector<char> silence(bytesFree);
|
||||
myDevice->write(silence.data(), silence.size());
|
||||
|
||||
const qint32 framesFree = myAudioFormat.framesForBytes(bytesFree);
|
||||
const qint64 duration = myAudioFormat.durationForFrames(framesFree);
|
||||
qDebug(appleAudio) << "Stopping with silence: frames =" << framesFree << ", duration =" << duration / 1000 << "ms";
|
||||
myAudioOutput->stop();
|
||||
}
|
||||
|
||||
void DirectSoundGenerator::writeEnoughSilence(const qint64 ms)
|
||||
{
|
||||
// write a few ms of silence
|
||||
const qint32 framesSilence = myAudioFormat.framesForDuration(ms * 1000); // target frames to write
|
||||
|
||||
const qint32 bytesFree = myAudioOutput->bytesFree();
|
||||
const qint32 framesFree = myAudioFormat.framesForBytes(bytesFree); // number of frames avilable to write
|
||||
const qint32 framesToWrite = std::min(framesFree, framesSilence);
|
||||
const qint64 bytesToWrite = myAudioFormat.bytesForFrames(framesToWrite);
|
||||
|
||||
std::vector<char> silence(bytesToWrite);
|
||||
myDevice->write(silence.data(), silence.size());
|
||||
|
||||
const qint64 duration = myAudioFormat.durationForFrames(framesToWrite);
|
||||
qDebug(appleAudio) << "Written some silence: frames =" << framesToWrite << ", duration =" << duration / 1000 << "ms";
|
||||
}
|
||||
|
||||
void DirectSoundGenerator::writeAudio()
|
||||
{
|
||||
if (!isRunning())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// we write all we have available (up to the free bytes)
|
||||
const DWORD bytesFree = myAudioOutput->bytesFree();
|
||||
|
||||
LPVOID lpvAudioPtr1, lpvAudioPtr2;
|
||||
DWORD dwAudioBytes1, dwAudioBytes2;
|
||||
// this function reads as much as possible up to bytesFree
|
||||
myBuffer->Read(bytesFree, &lpvAudioPtr1, &dwAudioBytes1, &lpvAudioPtr2, &dwAudioBytes2);
|
||||
|
||||
qint64 bytesWritten = 0;
|
||||
qint64 bytesToWrite = 0;
|
||||
if (lpvAudioPtr1)
|
||||
{
|
||||
bytesWritten += myDevice->write((char *)lpvAudioPtr1, dwAudioBytes1);
|
||||
bytesToWrite += dwAudioBytes1;
|
||||
}
|
||||
if (lpvAudioPtr2)
|
||||
{
|
||||
bytesWritten += myDevice->write((char *)lpvAudioPtr2, dwAudioBytes2);
|
||||
bytesToWrite += dwAudioBytes2;
|
||||
}
|
||||
|
||||
if (bytesToWrite != bytesWritten)
|
||||
{
|
||||
qDebug(appleAudio) << "Mismatch:" << bytesToWrite << "!=" << bytesWritten;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void registerSoundBuffer(IDirectSoundBuffer * buffer)
|
||||
{
|
||||
const std::shared_ptr<DirectSoundGenerator> generator = std::make_shared<DirectSoundGenerator>(buffer);
|
||||
activeSoundGenerators[buffer] = generator;
|
||||
}
|
||||
|
||||
void unregisterSoundBuffer(IDirectSoundBuffer * buffer)
|
||||
{
|
||||
activeSoundGenerators.erase(buffer);
|
||||
}
|
||||
|
||||
namespace QDirectSound
|
||||
{
|
||||
|
||||
void start()
|
||||
{
|
||||
for (auto & it : activeSoundGenerators)
|
||||
{
|
||||
const std::shared_ptr<DirectSoundGenerator> & generator = it.second;
|
||||
generator->start();
|
||||
}
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
for (auto & it : activeSoundGenerators)
|
||||
{
|
||||
const std::shared_ptr<DirectSoundGenerator> & generator = it.second;
|
||||
generator->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void writeAudio()
|
||||
{
|
||||
for (auto & it : activeSoundGenerators)
|
||||
{
|
||||
const std::shared_ptr<DirectSoundGenerator> & generator = it.second;
|
||||
generator->writeAudio();
|
||||
}
|
||||
}
|
||||
|
||||
void setOptions(const qint32 initialSilence, const qint32 silenceDelay, const qint32 volume)
|
||||
{
|
||||
for (auto & it : activeSoundGenerators)
|
||||
{
|
||||
const std::shared_ptr<DirectSoundGenerator> & generator = it.second;
|
||||
generator->setOptions(initialSilence, silenceDelay, volume);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
15
source/frontends/qapple/qdirectsound.h
Normal file
15
source/frontends/qapple/qdirectsound.h
Normal file
|
@ -0,0 +1,15 @@
|
|||
#ifndef DIRECTSOUND_H
|
||||
#define DIRECTSOUND_H
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
|
||||
namespace QDirectSound
|
||||
{
|
||||
void start();
|
||||
void stop();
|
||||
void writeAudio();
|
||||
void setOptions(const qint32 initialSilence, const qint32 silenceDelay, const qint32 volume);
|
||||
}
|
||||
|
||||
#endif // DIRECTSOUND_H
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include "linux/windows/wincompat.h"
|
||||
#include "linux/windows/dsound.h"
|
||||
#include "linux/windows/resources.h"
|
||||
#include "linux/windows/bitmap.h"
|
||||
|
||||
|
@ -23,19 +24,19 @@ void FrameRefreshStatus(int x, bool);
|
|||
|
||||
// Keyboard
|
||||
|
||||
BYTE KeybGetKeycode ();
|
||||
BYTE KeybGetKeycode ();
|
||||
BYTE KeybReadData();
|
||||
BYTE KeybReadFlag();
|
||||
|
||||
// Joystick
|
||||
|
||||
BYTE __stdcall JoyReadButton(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles);
|
||||
BYTE __stdcall JoyReadPosition(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles);
|
||||
BYTE JoyReadButton(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles);
|
||||
BYTE JoyReadPosition(WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles);
|
||||
void JoyResetPosition(ULONG uExecutedCycles);
|
||||
|
||||
// Speaker
|
||||
|
||||
BYTE __stdcall SpkrToggle (WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles);
|
||||
BYTE SpkrToggle (WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles);
|
||||
|
||||
// Registry
|
||||
|
||||
|
@ -48,3 +49,7 @@ void RegSaveValue (LPCTSTR section, LPCTSTR key, BOOL peruser, DWORD value);
|
|||
// MessageBox
|
||||
|
||||
int MessageBox(HWND, const char * text, const char * caption, UINT type);
|
||||
|
||||
// Mockingboard
|
||||
void registerSoundBuffer(IDirectSoundBuffer * buffer);
|
||||
void unregisterSoundBuffer(IDirectSoundBuffer * buffer);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "linux/windows/dsound.h"
|
||||
#include "linux/windows/winerror.h"
|
||||
#include "linux/interface.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
|
@ -8,15 +9,21 @@ HRESULT IDirectSoundNotify::SetNotificationPositions(DWORD cPositionNotifies, LP
|
|||
return DS_OK;
|
||||
}
|
||||
|
||||
IDirectSoundBuffer::IDirectSoundBuffer(const size_t bufferSize, const size_t channels, const size_t sampleRate, const size_t bitsPerSample, const size_t flags)
|
||||
: myBufferSize(bufferSize)
|
||||
, myChannels(channels)
|
||||
, mySampleRate(sampleRate)
|
||||
, myBitsPerSample(bitsPerSample)
|
||||
, myFlags(flags)
|
||||
IDirectSoundBuffer::IDirectSoundBuffer(const size_t aBufferSize, const size_t aChannels, const size_t aSampleRate, const size_t aBitsPerSample, const size_t aFlags)
|
||||
: bufferSize(aBufferSize)
|
||||
, channels(aChannels)
|
||||
, sampleRate(aSampleRate)
|
||||
, bitsPerSample(aBitsPerSample)
|
||||
, flags(aFlags)
|
||||
, mySoundNotify(new IDirectSoundNotify)
|
||||
, mySoundBuffer(bufferSize)
|
||||
, mySoundBuffer(aBufferSize)
|
||||
{
|
||||
registerSoundBuffer(this);
|
||||
}
|
||||
|
||||
IDirectSoundBuffer::~IDirectSoundBuffer()
|
||||
{
|
||||
unregisterSoundBuffer(this);
|
||||
}
|
||||
|
||||
HRESULT IDirectSoundBuffer::QueryInterface(int riid, void **ppvObject)
|
||||
|
@ -113,9 +120,37 @@ HRESULT IDirectSoundBuffer::Lock( DWORD dwWriteCursor, DWORD dwWriteBytes, LPVOI
|
|||
return DS_OK;
|
||||
}
|
||||
|
||||
HRESULT IDirectSoundBuffer::Read( DWORD dwReadBytes, LPVOID * lplpvAudioPtr1, DWORD * lpdwAudioBytes1, LPVOID * lplpvAudioPtr2, DWORD * lpdwAudioBytes2)
|
||||
{
|
||||
const DWORD available = (this->myWritePosition - this->myPlayPosition) % this->bufferSize;
|
||||
dwReadBytes = std::min(dwReadBytes, available);
|
||||
|
||||
const DWORD availableInFirstPart = this->mySoundBuffer.size() - this->myPlayPosition;
|
||||
|
||||
*lplpvAudioPtr1 = this->mySoundBuffer.data() + this->myPlayPosition;
|
||||
*lpdwAudioBytes1 = std::min(availableInFirstPart, dwReadBytes);
|
||||
|
||||
if (lplpvAudioPtr2 && lpdwAudioBytes2)
|
||||
{
|
||||
if (*lpdwAudioBytes1 < dwReadBytes)
|
||||
{
|
||||
*lplpvAudioPtr2 = this->mySoundBuffer.data();
|
||||
*lpdwAudioBytes2 = dwReadBytes - *lpdwAudioBytes1;
|
||||
}
|
||||
else
|
||||
{
|
||||
*lplpvAudioPtr2 = nullptr;
|
||||
*lpdwAudioBytes2 = 0;
|
||||
}
|
||||
}
|
||||
this->myPlayPosition = (this->myPlayPosition + dwReadBytes) % this->mySoundBuffer.size();
|
||||
return DS_OK;
|
||||
}
|
||||
|
||||
|
||||
HRESULT IDirectSoundBuffer::GetCurrentPosition( LPDWORD lpdwCurrentPlayCursor, LPDWORD lpdwCurrentWriteCursor )
|
||||
{
|
||||
*lpdwCurrentPlayCursor = this->myWritePosition;
|
||||
*lpdwCurrentPlayCursor = this->myPlayPosition;
|
||||
*lpdwCurrentWriteCursor = this->myWritePosition;
|
||||
return DS_OK;
|
||||
}
|
||||
|
|
|
@ -75,14 +75,8 @@ typedef struct IDirectSoundNotify *LPDIRECTSOUNDNOTIFY,**LPLPDIRECTSOUNDNOTIFY;
|
|||
|
||||
class IDirectSoundBuffer : public IUnknown
|
||||
{
|
||||
const size_t myBufferSize;
|
||||
const size_t mySampleRate;
|
||||
const size_t myChannels;
|
||||
const size_t myBitsPerSample;
|
||||
const size_t myFlags;
|
||||
|
||||
std::unique_ptr<IDirectSoundNotify> mySoundNotify;
|
||||
std::vector<SHORT> mySoundBuffer;
|
||||
std::vector<char> mySoundBuffer;
|
||||
|
||||
size_t myPlayPosition = 0;
|
||||
size_t myWritePosition = 0;
|
||||
|
@ -90,13 +84,23 @@ class IDirectSoundBuffer : public IUnknown
|
|||
LONG myVolume = DSBVOLUME_MIN;
|
||||
|
||||
public:
|
||||
const size_t bufferSize;
|
||||
const size_t sampleRate;
|
||||
const size_t channels;
|
||||
const size_t bitsPerSample;
|
||||
const size_t flags;
|
||||
|
||||
IDirectSoundBuffer(const size_t bufferSize, const size_t channels, const size_t sampleRate, const size_t bitsPerSample, const size_t flags);
|
||||
virtual ~IDirectSoundBuffer();
|
||||
|
||||
HRESULT QueryInterface(int riid, void **ppvObject);
|
||||
|
||||
HRESULT SetCurrentPosition( DWORD dwNewPosition );
|
||||
HRESULT GetCurrentPosition( LPDWORD lpdwCurrentPlayCursor, LPDWORD lpdwCurrentWriteCursor );
|
||||
|
||||
// Read is NOT part of Windows API
|
||||
HRESULT Read( DWORD dwReadBytes, LPVOID * lplpvAudioPtr1, DWORD * lpdwAudioBytes1, LPVOID * lplpvAudioPtr2, DWORD * lpdwAudioBytes2);
|
||||
|
||||
HRESULT Lock( DWORD dwWriteCursor, DWORD dwWriteBytes, LPVOID * lplpvAudioPtr1, DWORD * lpdwAudioBytes1, LPVOID * lplpvAudioPtr2, DWORD * lpdwAudioBytes2, DWORD dwFlags );
|
||||
HRESULT Unlock( LPVOID lpvAudioPtr1, DWORD dwAudioBytes1, LPVOID lpvAudioPtr2, DWORD dwAudioBytes2 );
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
#include "linux/windows/guiddef.h"
|
||||
#include "linux/windows/winerror.h"
|
||||
|
||||
IUnknown::~IUnknown()
|
||||
{
|
||||
}
|
||||
|
||||
HRESULT IUnknown::QueryInterface(int riid, void **ppvObject)
|
||||
{
|
||||
return S_OK;
|
||||
|
|
|
@ -15,6 +15,8 @@ struct IUnknown
|
|||
{
|
||||
HRESULT QueryInterface(int riid, void **ppvObject);
|
||||
HRESULT Release();
|
||||
|
||||
virtual ~IUnknown();
|
||||
};
|
||||
typedef IUnknown *LPUNKNOWN;
|
||||
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
#include "linux/interface.h"
|
||||
|
||||
void registerSoundBuffer(IDirectSoundBuffer * buffer)
|
||||
{
|
||||
}
|
||||
|
||||
void unregisterSoundBuffer(IDirectSoundBuffer * buffer)
|
||||
{
|
||||
}
|
||||
|
||||
// Resources
|
||||
|
||||
HRSRC FindResource(void *, const std::string & filename, const char *)
|
||||
|
|
Loading…
Add table
Reference in a new issue