Merge branch 'audio2'

This commit is contained in:
Andrea Odetti 2019-11-11 20:59:32 +00:00
commit 342697e983
20 changed files with 466 additions and 37 deletions

View file

@ -24,7 +24,6 @@ been modified, mostly for
* header files issues
* const char *
* STL constructor issues (see Disk_t and HDD)
* exclude some Windows heavy blocks (source/MouseInterface.cpp)
## What works
@ -41,7 +40,6 @@ Some features totally ignored:
* NSTC colors
* ethernet
* serial port
* sound
* debugger
* speech
@ -77,7 +75,9 @@ This is based on Qt, currently tested with 5.10
* joystick: it uses QtGamepad (correct names will only be displayed with 5.11)
* emulator runs in the main UI thread
* Qt timers are very coarse: the emulator needs to dynamically adapt the cycles to execute
* the app runs at 60FPS with correction for uneven timer deltas.
* full speed when disk spins execute up to 5 ms real wall clock of emulator code (then returns to Qt)
* (standard) audio is supported and there are a few configuration options to tune the latency (default very conservative 200ms)
## Build

View file

@ -17,18 +17,21 @@ add_executable(qapple
video.cpp
settings.cpp
configuration.cpp
audiogenerator.cpp
loggingcategory.cpp
commands.cpp
chunks.cpp
qhexedit.cpp
qhexedit2/commands.cpp
qhexedit2/chunks.cpp
qhexedit2/qhexedit.cpp
)
find_package(Qt5 REQUIRED
COMPONENTS Widgets Gamepad
COMPONENTS Widgets Gamepad Multimedia
)
target_link_libraries(qapple
Qt5::Widgets
Qt5::Gamepad
Qt5::Multimedia
appleii
)

View file

@ -0,0 +1,259 @@
#include "StdAfx.h"
#include "audiogenerator.h"
#include "Common.h"
#include "CPU.h"
#include "Applewin.h"
#include "Memory.h"
#include "loggingcategory.h"
#include <QDebug>
// Speaker
BYTE __stdcall SpkrToggle (WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles)
{
CpuCalcCycles(uExecutedCycles);
Q_UNUSED(pc)
Q_UNUSED(addr)
Q_UNUSED(bWrite)
Q_UNUSED(d)
AudioGenerator::instance().toggle();
return MemReadFloatingBus(uExecutedCycles);
}
AudioGenerator & AudioGenerator::instance()
{
static std::shared_ptr<AudioGenerator> audioGenerator(new AudioGenerator());
return *audioGenerator;
}
AudioGenerator::AudioGenerator()
{
myDevice = nullptr;
myInitialSilence = 200;
mySilenceDelay = 10000;
myVolume = 0x0fff;
QAudioFormat audioFormat;
audioFormat.setSampleRate(44100);
audioFormat.setChannelCount(1);
audioFormat.setSampleSize(sizeof(audio_t) * 8);
audioFormat.setCodec(QString::fromUtf8("audio/pcm"));
audioFormat.setByteOrder(QAudioFormat::LittleEndian);
audioFormat.setSampleType(QAudioFormat::SignedInt);
myAudioOutput.reset(new QAudioOutput(audioFormat));
myAudioFormat = myAudioOutput->format();
}
QAudioOutput * AudioGenerator::getAudioOutput()
{
return myAudioOutput.get();
}
void AudioGenerator::getOptions(qint32 & initialSilence, qint32 & silenceDelay, qint32 & volume) const
{
initialSilence = myInitialSilence;
silenceDelay = mySilenceDelay;
volume = myVolume;
}
void AudioGenerator::setOptions(const qint32 initialSilence, const qint32 silenceDelay, const qint32 volume)
{
myInitialSilence = std::max(0, initialSilence);
mySilenceDelay = std::max(0, silenceDelay);
myVolume = std::max(0, volume);
}
void AudioGenerator::stateChanged(QAudio::State state)
{
qDebug(appleAudio) << "Changed state: state =" << state << ", error =" << myAudioOutput->error() << ", free =" << myAudioOutput->bytesFree() << "bytes";
}
qint64 AudioGenerator::toFrameTime(qint64 cpuCycples)
{
const double CLKS_PER_SEC = g_fCurrentCLK6502;
qint64 timeInFrames = (cpuCycples - myStartCPUCycles) * myAudioFormat.sampleRate() / CLKS_PER_SEC;
return timeInFrames;
}
void AudioGenerator::toggle()
{
const qint64 timeInFrames = toFrameTime(g_nCumulativeCycles);
myTicks.push(timeInFrames);
}
bool AudioGenerator::isRunning()
{
QAudio::State state = myAudioOutput->state();
QAudio::Error error = myAudioOutput->error();
if (state == QAudio::ActiveState)
{
return true;
}
if (state == QAudio::IdleState && error == QAudio::NoError)
{
return true;
}
return false;
}
void AudioGenerator::start()
{
if (isRunning())
{
return;
}
qDebug(appleAudio) << "Restarting the AudioGenerator";
// restart as we are either starting or recovering from underrun
myDevice = myAudioOutput->start();
const int bytesSize = myAudioOutput->bufferSize();
const qint32 frameSize = myAudioFormat.framesForBytes(bytesSize);
myBuffer.resize(frameSize);
myStartCPUCycles = g_nCumulativeCycles;
myPreviousFrameTime = 0;
const qint32 framePeriod = myAudioFormat.framesForBytes(myAudioOutput->periodSize());
qDebug(appleAudio) << "AudioOutput: size =" << frameSize << "f, period =" << framePeriod << "f";
mySilence = 0;
myMaximum = 0;
myValue = 0;
myPhysical = myVolume;
myTicks = std::queue<qint64>();
writeEnoughSilence(myInitialSilence); // ms
}
void AudioGenerator::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);
generateSilence(myBuffer.data(), myBuffer.data() + framesToWrite);
const qint64 bytesToWrite = myAudioFormat.bytesForFrames(framesToWrite);
const char * data = reinterpret_cast<char *>(myBuffer.data());
myDevice->write(data, bytesToWrite);
const qint64 duration = myAudioFormat.durationForFrames(framesToWrite);
qDebug(appleAudio) << "Written some silence: frames =" << framesToWrite << ", duration =" << duration / 1000 << "ms";
}
void AudioGenerator::stop()
{
if (!isRunning())
{
return;
}
const qint32 bytesFree = myAudioOutput->bytesFree();
const qint32 framesFree = myAudioFormat.framesForBytes(bytesFree);
// fill with zeros and stop
generateSilence(myBuffer.data(), myBuffer.data() + framesFree);
const qint32 bytesToWrite = myAudioFormat.bytesForFrames(framesFree);
const char * data = reinterpret_cast<char *>(myBuffer.data());
myDevice->write(data, bytesToWrite);
const qint64 duration = myAudioFormat.durationForFrames(framesFree);
qDebug(appleAudio) << "Stopping with silence: frames =" << framesFree << ", duration =" << duration / 1000 << "ms";
myAudioOutput->stop();
}
void AudioGenerator::writeAudio()
{
if (!isRunning())
{
return;
}
// we write all we have available (up to the free bytes)
const qint64 currentFrameTime = toFrameTime(g_nCumulativeCycles);
const qint64 newFramesAvailable = currentFrameTime - myPreviousFrameTime;
const qint32 bytesFree = myAudioOutput->bytesFree();
const qint64 framesFree = myAudioFormat.framesForBytes(bytesFree);
const qint64 framesToWrite = std::min(framesFree, newFramesAvailable);
generateSamples(myBuffer.data(), framesToWrite);
const qint32 bytesToWrite = myAudioFormat.bytesForFrames(framesToWrite);
const char * data = reinterpret_cast<char *>(myBuffer.data());
const qint64 bytesWritten = myDevice->write(data, bytesToWrite);
if (bytesToWrite != bytesWritten)
{
qDebug(appleAudio) << "Mismatch:" << bytesToWrite << "!=" << bytesWritten;
}
const qint64 framesWritten = framesToWrite;
myPreviousFrameTime += framesWritten;
const qint32 bytesFreeNow = myAudioOutput->bytesFree();
if (bytesFreeNow > myMaximum)
{
// if this number is too big, it probably means we aren't providing enough data
const qint32 bytesSize = myAudioOutput->bufferSize();
myMaximum = bytesFreeNow;
qDebug(appleAudio) << "Running maximum free bytes:" << myMaximum << "/" << bytesSize;
}
}
void AudioGenerator::generateSilence(audio_t * begin, audio_t * end)
{
if (myValue != 0)
{
const audio_t delta = myPhysical > 0 ? -1 : +1;
for (audio_t * ptr = begin; ptr != end; ++ptr)
{
++mySilence;
if (myValue != 0 && mySilence > mySilenceDelay)
{
myValue += delta;
}
*ptr = myValue;
}
}
else
{
// no need to update mySilence as myValue is already 0
std::fill(begin, end, myValue);
}
}
void AudioGenerator::generateSamples(audio_t *data, qint64 framesToWrite)
{
qint64 start = myPreviousFrameTime;
qint64 end = myPreviousFrameTime + framesToWrite;
audio_t * head = data;
while (!myTicks.empty() && (myTicks.front() < end))
{
qint64 next = myTicks.front() - start;
audio_t * last = data + next;
std::fill(head, last, myValue);
head = last;
myPhysical = -myPhysical;
myValue = myPhysical;
mySilence = 0;
myTicks.pop();
}
generateSilence(head, data + framesToWrite);
}

View file

@ -0,0 +1,59 @@
#ifndef AUDIO_H
#define AUDIO_H
#include <QIODevice>
#include <QAudioFormat>
#include <QAudioOutput>
#include <queue>
#include <memory>
#include <vector>
class AudioGenerator {
public:
static AudioGenerator & instance();
QAudioOutput * getAudioOutput();
void toggle();
void start();
void stop();
void writeAudio();
void stateChanged(QAudio::State state);
void getOptions(qint32 & initialSilence, qint32 & silenceDelay, qint32 & volume) const;
void setOptions(const qint32 initialSilence, const qint32 silenceDelay, const qint32 volume);
private:
typedef short int audio_t;
std::queue<qint64> myTicks;
std::shared_ptr<QAudioOutput> myAudioOutput;
QAudioFormat myAudioFormat;
QIODevice * myDevice;
std::vector<audio_t> myBuffer;
// options
qint32 myInitialSilence;
qint32 mySilenceDelay;
qint16 myVolume;
qint64 myStartCPUCycles;
qint64 myPreviousFrameTime;
qint32 myMaximum;
qint32 mySilence;
audio_t myPhysical;
audio_t myValue;
AudioGenerator();
qint64 toFrameTime(qint64 cpuCycples);
void generateSamples(audio_t *data, qint64 framesToWrite);
void writeEnoughSilence(const qint64 ms);
void generateSilence(audio_t * begin, audio_t * end);
bool isRunning();
};
#endif // AUDIO_H

View file

@ -8,6 +8,7 @@
#include "Registry.h"
#include "SaveState.h"
#include "CPU.h"
#include "audiogenerator.h"
#include "linux/paddle.h"
@ -175,6 +176,8 @@ Preferences::Data getCurrentOptions(const std::shared_ptr<QGamepad> & gamepad)
currentOptions.screenshotTemplate = getScreenshotTemplate();
AudioGenerator::instance().getOptions(currentOptions.audioLatency, currentOptions.silenceDelay, currentOptions.volume);
return currentOptions;
}
@ -261,4 +264,6 @@ void setNewOptions(const Preferences::Data & currentOptions, const Preferences::
setScreenshotTemplate(newOptions.screenshotTemplate);
}
AudioGenerator::instance().setOptions(newOptions.audioLatency, newOptions.silenceDelay, newOptions.volume);
}

View file

@ -0,0 +1,3 @@
#include "loggingcategory.h"
Q_LOGGING_CATEGORY(appleAudio, "apple.audio")

View file

@ -0,0 +1,8 @@
#ifndef LOGGINGCATEGORY_H
#define LOGGINGCATEGORY_H
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(appleAudio)
#endif // LOGGINGCATEGORY_H

View file

@ -41,7 +41,7 @@
<customwidget>
<class>QHexEdit</class>
<extends>QWidget</extends>
<header>qhexedit.h</header>
<header>qhexedit2/qhexedit.h</header>
<container>1</container>
</customwidget>
</customwidgets>

View file

@ -188,6 +188,10 @@ void Preferences::setData(const Data & data)
save_state->setText(data.saveState);
screenshot->setText(data.screenshotTemplate);
audio_latency->setValue(data.audioLatency);
silence_delay->setValue(data.silenceDelay);
volume->setValue(data.volume);
}
Preferences::Data Preferences::getData() const
@ -220,6 +224,10 @@ Preferences::Data Preferences::getData() const
data.saveState = save_state->text();
data.screenshotTemplate = screenshot->text();
data.audioLatency = audio_latency->value();
data.silenceDelay = silence_delay->value();
data.volume = volume->value();
return data;
}

View file

@ -27,6 +27,10 @@ public:
bool enhancedSpeed;
int audioLatency;
int silenceDelay;
int volume;
std::vector<QString> disks;
std::vector<QString> hds;

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>648</width>
<height>315</height>
<height>316</height>
</rect>
</property>
<property name="windowTitle">
@ -385,6 +385,70 @@
</item>
</layout>
</widget>
<widget class="QWidget" name="audio">
<attribute name="title">
<string>Audio</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_7">
<item row="0" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>Audio latency (ms)</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="audio_latency">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="maximum">
<number>1000</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Silence delay (frames)</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="silence_delay">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="maximum">
<number>50000</number>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
<string>Volume</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="volume">
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="prefix">
<string>0x</string>
</property>
<property name="maximum">
<number>32767</number>
</property>
<property name="displayIntegerBase">
<number>16</number>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="registry">
<attribute name="title">
<string>Registry</string>

View file

@ -23,6 +23,7 @@
#include "emulator.h"
#include "memorycontainer.h"
#include "configuration.h"
#include "audiogenerator.h"
#include <QMdiSubWindow>
#include <QMessageBox>
@ -130,18 +131,6 @@ void FrameRefreshStatus(int, bool)
}
// Speaker
BYTE __stdcall SpkrToggle (WORD pc, WORD addr, BYTE bWrite, BYTE d, ULONG uExecutedCycles)
{
Q_UNUSED(pc)
Q_UNUSED(addr)
Q_UNUSED(bWrite)
Q_UNUSED(d)
Q_UNUSED(uExecutedCycles)
return 0;
}
void VideoInitialize()
{
VideoReinitialize();
@ -211,8 +200,11 @@ QApple::QApple(QWidget *parent) :
myEmulator = new Emulator(mdiArea);
myEmulatorWindow = mdiArea->addSubWindow(myEmulator, Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::WindowMinMaxButtonsHint);
connect(AudioGenerator::instance().getAudioOutput(), SIGNAL(stateChanged(QAudio::State)), this, SLOT(on_stateChanged(QAudio::State)));
const int fps = 60;
myMSGap = 1000 / fps;
myFullSpeedMS = 5;
on_actionPause_triggered();
initialiseEmulator();
@ -226,8 +218,15 @@ void QApple::closeEvent(QCloseEvent *)
uninitialiseEmulator();
}
void QApple::on_stateChanged(QAudio::State state)
{
AudioGenerator::instance().stateChanged(state);
}
void QApple::on_timer()
{
AudioGenerator::instance().start();
if (!myElapsedTimer.isValid())
{
myElapsedTimer.start();
@ -237,17 +236,20 @@ void QApple::on_timer()
// target x ms ahead of where we are now, which is when the timer should be called again
const qint64 target = myElapsedTimer.elapsed() + myMSGap;
const qint64 current = emulatorTimeInMS() - myCpuTimeReference;
const qint64 elapsed = target - current;
if (elapsed <= 0)
if (current > target)
{
// we got ahead of the timer by a lot
// just check if we got something to write
AudioGenerator::instance().writeAudio();
// wait next call
return;
}
const qint64 full_speed_ms = 5;
const qint64 toRun = target - current;
const double fUsecPerSec = 1.e6;
const qint64 nExecutionPeriodUsec = 1000 * elapsed;
const qint64 nExecutionPeriodUsec = 1000 * toRun;
const double fExecutionPeriodClks = g_fCurrentCLK6502 * (double(nExecutionPeriodUsec) / fUsecPerSec);
const DWORD uCyclesToExecute = fExecutionPeriodClks;
@ -265,7 +267,7 @@ void QApple::on_timer()
g_dwCyclesThisFrame = g_dwCyclesThisFrame % dwClksPerFrame;
++count;
}
while (sg_Disk2Card.IsConditionForFullSpeed() && (myElapsedTimer.elapsed() < target + full_speed_ms));
while (sg_Disk2Card.IsConditionForFullSpeed() && (myElapsedTimer.elapsed() < target + myFullSpeedMS));
// just repaint each time, to make it simpler
// we run @ 60 fps anyway
@ -275,12 +277,17 @@ void QApple::on_timer()
{
restartTimeCounters();
}
else
{
AudioGenerator::instance().writeAudio();
}
}
void QApple::stopTimer()
{
if (myTimerID)
{
restartTimeCounters();
killTimer(myTimerID);
myTimerID = 0;
}
@ -288,6 +295,8 @@ void QApple::stopTimer()
void QApple::restartTimeCounters()
{
// let them restart next time
AudioGenerator::instance().stop();
myElapsedTimer.invalidate();
}

View file

@ -5,6 +5,8 @@
#include <QElapsedTimer>
#include <QGamepad>
#include <QAudio>
#include <memory>
#include "preferences.h"
@ -58,6 +60,8 @@ private slots:
void on_actionSwap_disks_triggered();
void on_stateChanged(QAudio::State state);
private:
// helper class to pause the emulator and restart at the end of the block
@ -86,6 +90,7 @@ private:
qint64 myCpuTimeReference;
int myMSGap;
qint64 myFullSpeedMS;
};
#endif // QAPPLE_H

View file

@ -4,41 +4,43 @@
#
#-------------------------------------------------
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets gamepad
QT += core gui multimedia widgets gamepad
TARGET = qapple
TEMPLATE = app
SOURCES += main.cpp\
qapple.cpp \
audiogenerator.cpp \
loggingcategory.cpp \
qapple.cpp \
qresources.cpp \
emulator.cpp \
video.cpp \
graphicscache.cpp \
chunks.cpp \
commands.cpp \
qhexedit.cpp \
memorycontainer.cpp \
preferences.cpp \
gamepadpaddle.cpp \
settings.cpp \
configuration.cpp
configuration.cpp \
qhexedit2/chunks.cpp \
qhexedit2/commands.cpp \
qhexedit2/qhexedit.cpp
HEADERS += qapple.h \
audiogenerator.h \
emulator.h \
loggingcategory.h \
video.h \
graphicscache.h \
chunks.h \
commands.h \
qhexedit.h \
memorycontainer.h \
preferences.h \
gamepadpaddle.h \
settings.h \
configuration.h
configuration.h \
qhexedit2/chunks.h \
qhexedit2/commands.h \
qhexedit2/qhexedit.h
FORMS += qapple.ui \
emulator.ui \