Architecting the drum machine project
从drum播放和录制音轨
将此曲目保存到文件并加载以进行播放
要播放声音,我们将布置四个大按钮,它们将在单击(或键盘事件)时播放特定的鼓声:底鼓、军鼓、踩镲和镲片碰撞。 这些声音将是应用程序加载的 .wav 文件。 用户将能够记录他的声音序列并重播。
SoundEvent 类是轨道的基本struct。它是一个简单的类,包含时间戳(播放声音的时间)和 soundId 变量(播放的声音)。
Track 类包含 SoundEvent 列表、持续时间和状态(播放、录制、停止)。每次用户播放声音时,都会创建一个 SoundEvent 类并将其添加到 Track 类中。
PlaybackWorker 类是一个在不同线程中运行的工作类。它负责循环遍历 Track 类的 soundEvents 并在达到其时间戳时触发正确的声音。
Serializable 类是每个要序列化的类都必须实现的接口(在我们的例子中:SoundEvent 和 Track)。
Serializer 类是每个特定格式的实现类都必须实现的接口
JsonSerializer、XmlSerializer 和 BinarySerializer 是 Serializer 类的子类,它们执行特定于格式的工作来序列化/反序列化 Serializable 实例。
SoundEffectWidget 类是保存播放单个声音的信息的小部件。它显示了我们四种声音之一的按钮。它还拥有将声音发送到声卡的 QSoundEffect 类。
MainWindow 类将所有内容放在一起。它拥有 Track 类,产生 PlaybackWorker 线程,并触发序列化/反序列化。
SoundEvent.h
#ifndef SOUNDEVENT_H
#define SOUNDEVENT_H
#include <QtGlobal>
#include "Serializable.h"
//在录音模式下,每次用户播放声音时,都会使用适当的数据创建一个 SoundEvent
class SoundEvent : public Serializable
{
public:
SoundEvent(qint64 timestamp = 0, int soundId = 0);
~SoundEvent();
QVariant toVariant() const override;
void fromVariant(const QVariant& variant) override;
//时间戳:一个 qint64(long long 类型),包含 SoundEvent 的当前时间,以毫秒为单位
qint64 timestamp;
//soundId:已播放声音的ID
int soundId;
};
#endif // SOUNDEVENT_H
SoundEvent.h
#ifndef SOUNDEVENT_H
#define SOUNDEVENT_H
#include <QtGlobal>
#include "Serializable.h"
//在录音模式下,每次用户播放声音时,都会使用适当的数据创建一个 SoundEvent
class SoundEvent : public Serializable
{
public:
SoundEvent(qint64 timestamp = 0, int soundId = 0);
~SoundEvent();
QVariant toVariant() const override;
void fromVariant(const QVariant& variant) override;
//时间戳:一个 qint64(long long 类型),包含 SoundEvent 的当前时间,以毫秒为单位
qint64 timestamp;
//soundId:已播放声音的ID
int soundId;
};
#endif // SOUNDEVENT_H
SoundEvent.h
#ifndef SOUNDEVENT_H
#define SOUNDEVENT_H
#include <QtGlobal>
#include "Serializable.h"
//在录音模式下,每次用户播放声音时,都会使用适当的数据创建一个 SoundEvent
class SoundEvent : public Serializable
{
public:
SoundEvent(qint64 timestamp = 0, int soundId = 0);
~SoundEvent();
QVariant toVariant() const override;
void fromVariant(const QVariant& variant) override;
//时间戳:一个 qint64(long long 类型),包含 SoundEvent 的当前时间,以毫秒为单位
qint64 timestamp;
//soundId:已播放声音的ID
int soundId;
};
#endif // SOUNDEVENT_H
SoundEvent.cpp
#include "SoundEvent.h"
SoundEvent::SoundEvent(qint64 timestamp, int soundId) :
Serializable(),
timestamp(timestamp),
soundId(soundId)
{
}
SoundEvent::~SoundEvent()
{
}
QVariant SoundEvent::toVariant() const
{
QVariantMap map;
map.insert("timestamp", timestamp);
map.insert("soundId", soundId);
return map;
}
void SoundEvent::fromVariant(const QVariant& variant)
{
QVariantMap map = variant.toMap();
timestamp = map.value("timestamp").toLongLong();
soundId = map.value("soundId").toInt();
}
Track.h
#ifndef TRACK_H
#define TRACK_H
#include <memory>
#include <vector>
#include <QObject>
#include <QVector>
#include <QElapsedTimer>
#include "Serializable.h"
#include "SoundEvent.h"
//Track 类是项目业务逻辑的枢纽。它持有 mState,即整个应用程序的状态。它的内容将在播放您精彩的音乐表演时被读取,并且还会被序列化到一个文件中。
class Track : public QObject, public Serializable
{
Q_OBJECT
public:
enum class State {
STOPPED,
PLAYING,
RECORDING,
};
explicit Track(QObject *parent = 0);
~Track();
QVariant toVariant() const override;
void fromVariant(const QVariant& variant) override;
State state() const;
State previousState() const;
//该函数返回 mTimer.elapsed() 的值
quint64 elapsedTime() const;
const std::vector<std::unique_ptr<SoundEvent>>& soundEvents() const;
qint64 duration() const;
signals:
//当 mState 值更新时发出此函数。新状态作为参数传递。
void stateChanged(State state);
public slots:
//这个函数是一个开始播放曲目的槽
//真正的播放是由PlaybackWorker触发的
void play();
//用来启动Track的录音状态
void record();
//停止当前开始或记录状态
void stop();
//此函数使用给定的 soundId 创建一个新的 SoundEvent 并将其添加到 mSoundEvents
void addSoundEvent(int soundEventId);
private:
//此函数重置 Track 的内容:它清除 mSoundEvents 并将 mDuration 设置为 0
void clear();
void setState(State state);
private:
//mDuration:此变量保存 Track 类的持续时间。该成员在开始录制时重置为 0,并在录制停止时更新。
qint64 mDuration;
//mSoundEvents:这个变量是给定轨道的 SoundEvents 列表。正如 unique_ptr 语义所述,Track是声音事件的所有者。
std::vector<std::unique_ptr<SoundEvent>> mSoundEvents;
//mTimer:每次播放或录制 Track 时都会启动此变量
QElapsedTimer mTimer;
//mState:这个变量是当前Track类的State,它可以有三个可能的值:STOPPED、PLAYING、RECORDING。
State mState;
//mPreviousState:这个变量是轨道的前一个状态。当您想知道对新的 STOPPEDState 执行哪个操作时,这很有用。如果 mPreviousState 处于 PLAYING 状态,我们将不得不停止播放。
State mPreviousState;
};
#endif // TRACK_H
Track.cpp
#include "Track.h"
using namespace std;
Track::Track(QObject *parent) :
QObject(parent),
Serializable(),
mDuration(0),
mSoundEvents(),
mTimer(),
mState(State::STOPPED),
mPreviousState(mState)
{
}
Track::~Track()
{
}
QVariant Track::toVariant() const
{
QVariantMap map;
map.insert("duration", mDuration);
QVariantList list;
for (const auto& soundEvent : mSoundEvents) {
list.append(soundEvent->toVariant());
}
map.insert("soundEvents", list);
return map;
}
void Track::fromVariant(const QVariant& variant)
{
QVariantMap map = variant.toMap();
mDuration = map.value("duration").toLongLong();
QVariantList list = map.value("soundEvents").toList();
for(const QVariant& data : list) {
auto soundEvent = make_unique<SoundEvent>();
soundEvent->fromVariant(data);
mSoundEvents.push_back(move(soundEvent));
}
}
//Track 类不包含与 Qt 多媒体 API 相关的任何内容
void Track::play()
{
//调用 Track.play() 只是将状态更新为 PLAYING 并启动 mTimer
setState(State::PLAYING);
mTimer.start();
}
void Track::record()
{
clear();
setState(State::RECORDING);
mTimer.start();
}
void Track::stop()
{
//它首先清除数据,将状态设置为 RECORDING,并启动 Timer
//如果我们停止在 RECORDING 状态,则更新 mDuration
if (mState == State::RECORDING) {
mDuration = mTimer.elapsed();
}
setState(State::STOPPED);
}
void Track::addSoundEvent(int soundEventId)
{
if (mState != State::RECORDING) {
return;
}
mSoundEvents.push_back(make_unique<SoundEvent>(
mTimer.elapsed(),
soundEventId));
}
void Track::clear()
{
mSoundEvents.clear();
mDuration = 0;
}
void Track::setState(Track::State state)
{
//mState 的当前值在更新之前存储在 mPreviousState 中。 最后, stateChanged() 与新值一起发出。
mPreviousState = mState;
mState = state;
emit stateChanged(mState);
}
qint64 Track::duration() const
{
return mDuration;
}
//仅当我们处于 RECORDING 状态时才会创建 soundEvent。 之后,使用mTimer的当前经过时间和传递的 soundEventId 将SoundEvent添加到 mSoundEvents。
const std::vector<std::unique_ptr<SoundEvent> >& Track::soundEvents() const
{
return mSoundEvents;
}
Track::State Track::state() const
{
return mState;
}
Track::State Track::previousState() const
{
return mPreviousState;
}
quint64 Track::elapsedTime() const
{
return mTimer.elapsed();
}
PlaybackWorker.h
#ifndef PLAYBACKWORKER_H
#define PLAYBACKWORKER_H
#include <QObject>
#include <QAtomicInteger>
class Track;
//PlaybackWorker 类将在不同的线程中运行
//回放
class PlaybackWorker : public QObject
{
Q_OBJECT
public:
explicit PlaybackWorker(const Track& track, QObject *parent = 0);
signals:
//每次需要播放声音时,PlaybackWorker 都会发出此信号
void playSound(int soundId);
//当播放播放到结束时发出此函数。 如果中途停止,则不会发出此信号。
void trackFinished();
public slots:
//这个函数是PlaybackWorker的主要函数,在其中,将查询 mTrack 内容以触发声音
void play();
//此函数是更新 mIsPlaying 标志并使 play() 退出其循环的函数
void stop();
private:
const Track& mTrack;
QAtomicInteger<bool> mIsPlaying;
};
#endif // PLAYBACKWORKER_H
PlaybackWorker.cpp
#include "PlaybackWorker.h"
#include <QElapsedTimer>
#include <QThread>
#include "Track.h"
#include "SoundEvent.h"
//这个是对 PlaybackWorker 正在工作的 Track 类的引用
//它在构造函数中作为常量引用传递
//有了这些信息,您已经知道 PlaybackWorker 不能以任何方式修改 mTrack
//mIsPlaying 这个函数是一个标志,用来阻止另一个线程的工作线程。它是一个 QAtomicInteger 来保证对变量的原子访问
PlaybackWorker::PlaybackWorker(const Track& track, QObject *parent) :
QObject(parent),
mTrack(track),
mIsPlaying(false)
{
}
void PlaybackWorker::play()
{
//play() 函数所做的第一件事是准备读取
//mIsPlaying 设置为 true,声明一个 QElapsedTimer 类,并初始化一个 soundEventIndex
mIsPlaying.store(true);
QElapsedTimer timer;
size_t soundEventIndex = 0;
const auto& soundEvents = mTrack.soundEvents();
timer.start();
//每次调用 timer.elapsed() 时,我们都会知道是否应该播放声音
while(timer.elapsed() <= mTrack.duration()
&& mIsPlaying.load()) {
if (soundEventIndex < soundEvents.size()) {
const auto& soundEvent = soundEvents.at(soundEventIndex);
if (timer.elapsed() >= soundEvent->timestamp) {
emit playSound(soundEvent->soundId);
soundEventIndex++;
}
}
QThread::msleep(1);
}
if (soundEventIndex >= soundEvents.size()) {
emit trackFinished();
}
}
void PlaybackWorker::stop()
{
mIsPlaying.store(false);
}
使用 QVariant 使您的对象可序列化
Track 和 SoundEvent 类现在可以转换为常见的 Qt 格式 QVariant。我们现在需要在具有文本或二进制格式的文件中编写一个 Track(及其 SoundEvent 对象)类。 此示例项目允许您处理所有格式。它将允许您在一行中切换保存的文件格式。
在这种情况,特定的文件格式序列化代码在一个专门的子类中,每次我们添加一个新对象进行序列化时,我们都必须创建所有这些子类来处理不同的序列化文件格式。这庞大的继承树很快就会变得一团糟。代码将无法维护。所以,这里是桥接模式可以成为一个很好的解决方案的地方:
在桥接模式中,我们将两个继承层次结构中的类解耦:该组件独立于文件格式。 SoundEvent 和 Track 对象不关心 JSON、XML 或二进制格式。文件格式实现。 JsonSerializer、XmlSerializer 和 BinarySerializer 处理通用格式、Serializable,而不是特定组件,例如 SoundEvent 或 Track。请注意,在经典桥接模式中,抽象 (Serializable) 应包含实现者 (Serializer) 变量。调用者只处理抽象。 但是在这个项目示例中,MainWindow 拥有 Serializable 和 Serializer 的所有权。 这是在保持非耦合功能类的同时使用设计模式的力量的个人选择。
Serializable 和 Serializer 的架构很清晰。 Serializable 类已经实现,因此您现在可以创建一个名为 Serializer.h 的新 C++ 头文件:
Serializer.h
#ifndef SERIALIZATION_H
#define SERIALIZATION_H
#include <QString>
#include "Serializable.h"
//Serializer 类是一个接口,一个只有纯虚函数而没有数据的抽象类
class Serializer
{
public:
virtual ~Serializer() {}
//此函数将 Serializable 保存到硬盘驱动器上的文件中。
//Serializable 类是常量,不能被这个函数修改
//filepath 函数指示要创建的目标文件
//一些 Serializer 实现可以使用 rootName 变量
//例如,如果我们请求保存一个 Track 对象,那么 rootName 变量可以是字符串 track。 这是用于写入根元素的标签。 XML 实现需要此信息。
virtual void save(const Serializable& serializable, const QString& filepath, const QString& rootName = "") = 0;
//此函数从文件加载数据以填充 Serializable 类
//Serializable 类将由此函数更新
//filepath 函数指示要读取的文件
virtual void load(Serializable& serializable, const QString& filepath) = 0;
};
#endif // SERIALIZATION_H
接口Serializer已经准备好了,等待一些实现! 让我们从 JSON 开始。 创建一个 C++ 类 JsonSerializer。 这是 JsonSerializer.h 的头文件:
JsonSerializer.h
#ifndef JSONSERIALIZER_H
#define JSONSERIALIZER_H
#include "Serializer.h"
class JsonSerializer : public Serializer
{
public:
JsonSerializer();
void save(const Serializable& serializable, const QString& filepath, const QString& rootName) override;
void load(Serializable& serializable, const QString& filepath) override;
};
#endif // JSONSERIALIZER_H
我们必须提供 save() 和 load() 的实现。 这是 save() 的实现:
JsonSerializer.cpp
#include "JsonSerializer.h"
#include <QJsonDocument>
#include <QFile>
JsonSerializer::JsonSerializer() :
Serializer()
{
}
void JsonSerializer::save(const Serializable& serializable, const QString& filepath, const QString& /*rootName*/)
{
//Qt 框架提供了一种使用 QJsonDocument 类读写 JSON 文件的好方法。
//我们可以从 QVariant 类创建 QJsonDocument 类
//注意 QJsonDocument 接受的 QVariant 必须是 QVariantMap、QVariantList 或 QStringList。
//Track 类和 SoundEvent 的 toVariant() 函数会生成一个 QVariantMap
QJsonDocument doc = QJsonDocument::fromVariant(serializable.toVariant());
//然后,我们可以创建一个带有目标文件路径的 QFile 文件
QFile file(filepath);
file.open(QFile::WriteOnly);
//QJsonDocument::toJson() 函数将其转换为 UTF-8 编码的文本表示
file.write(doc.toJson());
//QJsonDocument::toJson() 函数可以生成缩进或紧凑的 JSON 格式。 默认情况下,格式为 QJsonDocument::Indented。
//我们将此结果写入 QFile 文件并关闭该文件
file.close();
}
//我们用源文件路径打开一个 QFile。
void JsonSerializer::load(Serializable& serializable, const QString& filepath)
{
QFile file(filepath);
file.open(QFile::ReadOnly);
//我们使用 QFile::readAll() 读取所有数据
//然后我们可以使用 QJsonDocument::fromJson() 函数创建一个 QJsonDocument 类
QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
file.close();
//最后,我们可以用转换为 QVariant 类的 QJsonDocument 填充我们的目标 Serializable
//QJsonDocument::toVariant() 函数可以根据 JSON 文档的性质返回 QVariantList 或 QVariantMap
serializable.fromVariant(doc.toVariant());
}
这是使用此 JsonSerializer 保存的 Track 类的示例:
{
//根元素是一个 JSON 对象,由具有两个键的映射表示
"duration": 6205,
//持续时间:这是一个简单的整数值
//soundEvents:这是一个对象数组。 每个对象都是一个具有以下键的映射:
//soundId:这是一个整数
//时间戳:这也是一个整数
"soundEvents": [
{
"soundId": 0,
"timestamp": 2689
},
{
"soundId": 2,
"timestamp": 2690
},
{
"soundId": 2,
"timestamp": 3067
}
]
}
以 XML 格式序列化对象
JSON 序列化是 C++ 对象的直接表示,而 Qt 已经提供了我们需要的一切。但是,C++ 对象的序列化可以通过 XML 格式的各种表示来完成。所以我们必须自己编写 XML <-> QVariant 转换。 我们决定使用以下 XML 表示:
<[name]> type="[type]">[data]</[name]>
例如, soundId 类型给出了这个 XML 表示:
<soundId type="int">2</soundId>
创建一个也继承自 Serializer 的 C++ 类 XmlSerializer。
XmlSerializer.h
#ifndef XMLSERIALIZER_H
#define XMLSERIALIZER_H
#include <QXmlStreamWriter>
#include <QXmlStreamReader>
#include "Serializer.h"
class XmlSerializer : public Serializer
{
public:
XmlSerializer();
void save(const Serializable& serializable, const QString& filepath, const QString& rootName) override;
void load(Serializable& serializable, const QString& filepath) override;
private:
void writeVariantToStream(const QString& nodeName, const QVariant& variant, QXmlStreamWriter& stream);
void writeVariantValueToStream(const QVariant& variant, QXmlStreamWriter& stream);
void writeVariantListToStream(const QVariant& variant, QXmlStreamWriter& stream);
void writeVariantMapToStream(const QVariant& variant, QXmlStreamWriter& stream);
QVariant readVariantFromStream(QXmlStreamReader& stream);
QVariant readVariantValueFromStream(QXmlStreamReader& stream);
QVariant readVariantListFromStream(QXmlStreamReader& stream);
QVariant readVariantMapFromStream(QXmlStreamReader& stream);
};
#endif // XMLSERIALIZER_H
XmlSerializer.cpp
#include "XmlSerializer.h"
#include <QFile>
XmlSerializer::XmlSerializer() :
Serializer()
{
}
void XmlSerializer::save(const Serializable& serializable, const QString& filepath, const QString& rootName)
{
//我们使用文件路径目标创建一个 QFile 文件。
QFile file(filepath);
file.open(QFile::WriteOnly);
//我们构造了一个写入 QFile 的 QXmlStreamWriter 对象
//默认情况下,编写器将生成一个紧凑的 XML
QXmlStreamWriter stream(&file);
//您可以使用 QXmlStreamWriter::setAutoFormatting() 函数生成漂亮的 XML。
stream.setAutoFormatting(true);
//QXmlStreamWriter::writeStartDocument() 函数写入 XML 版本和编码
stream.writeStartDocument();
//我们使用 writeVariantToStream() 函数在 XML 流中编写 QVariant
writeVariantToStream(rootName, serializable.toVariant(), stream);
stream.writeEndDocument();
//最后,我们结束文档并关闭 QFile
file.close();
}
void XmlSerializer::load(Serializable& serializable, const QString& filepath)
{
//首先要做的是创建一个带有源文件路径的 QFile
QFile file(filepath);
file.open(QFile::ReadOnly);
//我们用 QFile 构造一个 QXmlStreamReader
QXmlStreamReader stream(&file);
//QXmlStreamReader ::readNextStartElement() 函数读取直到 XML 流中的下一个开始元素
stream.readNextStartElement();
//然后我们可以使用我们的读取辅助函数 readVariantFromStream() 从 XML 流创建一个 QVariant 类
//最后,我们可以使用我们的 Serializable::fromVariant() 来填充目标可序列化。 让我们实现辅助函数 readVariantFromStream():
serializable.fromVariant(readVariantFromStream(stream));
}
//如前所述,将 QVariant 写入 XML 流取决于您希望如何表示数据。 所以我们必须编写转换函数。 请像这样使用 writeVariantToStream() 更新您的课程:
void XmlSerializer::writeVariantToStream(const QString& nodeName, const QVariant& variant, QXmlStreamWriter& stream)
{
//这个 writeVariantToStream() 函数是一个通用入口点
//每次我们想在 XML 流中放入 QVariant 时都会调用它
//使用 writeStartElement() 函数开始一个新的 XML 元素
//nodeName 将用于创建 XML 标记例如<soundId
stream.writeStartElement(nodeName);
//在当前元素中编写一个名为 type 的 XML 属性。我们使用存储在 QVariant 中的类型的名称
stream.writeAttribute("type", variant.typeName());
//QVariant 类可以是列表、map或数据
//因此,如果 QVariant 是容器(QVariantList 或 QVariantMap),我们将应用特定处理。所有其他情况都被视为数据值
switch (variant.type()) {
case QMetaType::QVariantList:
writeVariantListToStream(variant, stream);
break;
case QMetaType::QVariantMap:
writeVariantMapToStream(variant, stream);
break;
default:
writeVariantValueToStream(variant, stream);
break;
}
//我们用 writeEndElement() 结束当前的 XML 元素
stream.writeEndElement();
}
void XmlSerializer::writeVariantValueToStream(const QVariant& variant, QXmlStreamWriter& stream)
{
//如果 QVariant 是一个简单类型,我们检索它的 QString 表示。 然后我们使用 QXmlStreamWriter::writeCharacters() 在 XML 流中写入这个 QString。
stream.writeCharacters(variant.toString());
}
void XmlSerializer::writeVariantListToStream(const QVariant& variant, QXmlStreamWriter& stream)
{
//在这一步,我们已经知道 QVariant 是一个 QVariantList
//我们调用 QVariant::toList() 来检索列表
QVariantList list = variant.toList();
//然后我们遍历列表的所有元素并调用我们的通用入口点 writeVariantToStream()
for(const QVariant& element : list) {
writeVariantToStream("item", element, stream);
}
}
void XmlSerializer::writeVariantMapToStream(const QVariant& variant, QXmlStreamWriter& stream)
{
//QVariant 是一个容器,但这次是 QVariantMap
QVariantMap map = variant.toMap();
QMapIterator<QString, QVariant> i(map);
//我们为遍历的每个元素调用 writeVariantToStream()。
//标签名称很重要,因为这是一个map
while (i.hasNext()) {
i.next();
//我们使用 QMapIterator::key() 中的映射键作为节点名称
writeVariantToStream(i.key(), i.value(), stream);
}
}
QVariant XmlSerializer::readVariantFromStream(QXmlStreamReader& stream)
{
QXmlStreamAttributes attributes = stream.attributes();
QString typeString = attributes.value("type").toString();
//这个函数的作用是创建一个QVariant
QVariant variant;
//首先,我们从 XML 属性中检索“类型”
//在我们的例子中,我们只有一个属性需要处理
//然后,根据类型,我们将调用三个读取辅助函数之一
switch (QVariant::nameToType(typeString.toStdString().c_str())) {
case QMetaType::QVariantList:
variant = readVariantListFromStream(stream);
break;
case QMetaType::QVariantMap:
variant = readVariantMapFromStream(stream);
break;
default:
variant = readVariantValueFromStream(stream);
break;
}
return variant;
}
QVariant XmlSerializer::readVariantValueFromStream(QXmlStreamReader& stream)
{
QXmlStreamAttributes attributes = stream.attributes();
//与前面的函数一样,我们从 XML 属性中检索类型
QString typeString = attributes.value("type").toString();
//我们还使用 QXmlStreamReader::readElementText() 函数将数据作为文本读取
QString dataString = stream.readElementText();
//此函数根据类型创建一个 QVariant 及其数据
//使用此 QString 数据创建 QVariant 类
//在这一步,QVariant 类型是一个 QString
QVariant variant(dataString);
//所以我们使用 QVariant::convert() 函数将 QVariant 转换为真实类型(int、qlonglong 等)
variant.convert(QVariant::nameToType(typeString.toStdString().c_str()));
return variant;
}
QVariant XmlSerializer::readVariantListFromStream(QXmlStreamReader& stream)
{
//我们知道流元素包含一个数组
//所以,这个函数创建并返回一个 QVariantList
QVariantList list;
//QXmlStreamReader::readNextStartElement() 函数读取直到下一个开始元素
//如果在当前元素中找到一个开始元素,则返回 true
//我们为每个元素调用入口函数 readVariantFromStream()。 最后,我们返回 QVariantList。
while(stream.readNextStartElement()) {
list.append(readVariantFromStream(stream));
}
return list;
}
QVariant XmlSerializer::readVariantMapFromStream(QXmlStreamReader& stream)
{
QVariantMap map;
while(stream.readNextStartElement()) {
map.insert(stream.name().toString(), readVariantFromStream(stream));
}
return map;
}
使用 XmlSerializer 序列化的 Track 类如下所示
<?xml version="1.0" encoding="UTF-8"?>
<track type="QVariantMap">
<duration type="qlonglong">6205</duration>
<soundEvents type="QVariantList">
<item type="QVariantMap">
<soundId type="int">0</soundId>
<timestamp type="qlonglong">2689</timestamp>
</item>
<item type="QVariantMap">
<soundId type="int">2</soundId>
<timestamp type="qlonglong">2690</timestamp>
</item>
<item type="QVariantMap">
<soundId type="int">2</soundId>
<timestamp type="qlonglong">3067</timestamp>
</item>
</soundEvents>
</track>
以二进制格式序列化对象
二进制序列化更容易,因为 Qt 提供了一种直接的方法来做到这一点。 请创建一个从 Serializer 继承的 BinarySerializer 类
BinarySerializer.h
#ifndef BINARYSERIALIZER_H
#define BINARYSERIALIZER_H
#include "Serializer.h"
//这次我们使用这个类来序列化目标 QFile 中的二进制数据
class BinarySerializer : public Serializer
{
public:
BinarySerializer();
void save(const Serializable& serializable, const QString& filepath, const QString& rootName) override;
void load(Serializable& serializable, const QString& filepath) override;
};
#endif // BINARYSERIALIZER_H
BinarySerializer.cpp
#include "BinarySerializer.h"
#include <QFile>
#include <QDataStream>
BinarySerializer::BinarySerializer() :
Serializer()
{
}
void BinarySerializer::save(const Serializable& serializable, const QString& filepath, const QString& /*rootName*/)
{
QFile file(filepath);
file.open(QFile::WriteOnly);
QDataStream dataStream(&file);
//QDataStream 类接受带有 << 运算符的 QVariant 类
//二进制序列化程序中未使用 rootName 变量
dataStream << serializable.toVariant();
file.close();
}
//由于 QVariant 和 QDataStream 机制,这项任务很容易
void BinarySerializer::load(Serializable& serializable, const QString& filepath)
{
//我们用源文件路径打开 QFile。 我们用这个 QFile 构造一个 QDatastream 类
//后,我们使用 >> 操作符读取根 QVariant。 最后,我们用 Serializable::fromVariant() 函数填充源 Serializable。
QFile file(filepath);
file.open(QFile::ReadOnly);
QDataStream dataStream(&file);
QVariant variant;
dataStream >> variant;
serializable.fromVariant(variant);
file.close();
}
使用 QSoundEffect 播放低延迟声音
项目应用程序 显示了四个 SoundEffectWidget 小部件:kickWidget、snareWidget、hihatWidget 和 crashWidget。
每个 SoundEffectWidget 小部件显示一个 QLabel 和一个 QPushButton。标签显示声音名称。如果单击该按钮,则会播放声音。
Qt Multimedia 模块提供了两种播放音频文件的主要方式
QMediaPlayer:这个文件可以播放各种输入格式的歌曲、电影和网络电台
QSoundEffect:这个文件可以播放低延迟的.wav文件
这个项目示例是一个虚拟drum,所以我们使用了一个 QSoundEffect 对象。
然后你可以初始化声音。 下面是一个例子:
QUrl urlKick("qrc:/sounds/kick.wav");
QUrl urlBetterKick = QUrl::fromLocalFile("/home/better-kick.wav");
QSoundEffect soundEffect;
QSoundEffect.setSource(urlBetterKick);
第一步是为您的声音文件创建一个有效的 QUrl,urlKick 从 .qrc 资源文件路径初始化 而 urlBetterKick 从本地文件路径创建
然后我们可以创建 QSoundEffect 并设置 URL 声音以使用 QSoundEffect::setSource() 函数播放。
现在我们已经初始化了一个 QSoundEffect 对象,我们可以使用以下代码片段播放声音:
soundEffect.setVolume(1.0f);
soundEffect.play();
用键盘触发按钮
SoundEffectWidget.h
#ifndef SOUNDEFFECTWIDGET_H
#define SOUNDEFFECTWIDGET_H
#include <QWidget>
#include <QSoundEffect>
#include <QPushButton>
#include <QLabel>
class SoundEffectWidget : public QWidget
{
Q_OBJECT
public:
SoundEffectWidget(QWidget* parent = 0);
void loadSound(const QUrl& url);
void setId(int id);
void setName(const QString& name);
Qt::Key triggerKey() const;
void setTriggerKey(const Qt::Key& triggerKey);
signals:
void soundPlayed(int soundId);
public slots:
void play();
void triggerPlayButton();
protected:
void dragEnterEvent(QDragEnterEvent* event) override;
void dropEvent(QDropEvent* event) override;
private:
int mId;
QSoundEffect mSoundEffect;
QPushButton* mPlayButton;
QLabel* mFilenameLabel;
Qt::Key mTriggerKey;
};
#endif // SOUNDEFFECTWIDGET_H
SoundEffectWidget.cpp
#include "SoundEffectWidget.h"
#include <QVBoxLayout>
#include <QDropEvent>
#include <QMimeData>
#include <QMimeDatabase>
SoundEffectWidget::SoundEffectWidget(QWidget* parent) :
QWidget(parent),
mId(-1),
mSoundEffect(this),
mPlayButton(new QPushButton()),
mFilenameLabel(new QLabel()),
mTriggerKey(Qt::Key_unknown)
{
//如果将 .wav 文件拖放到 SoundEffectWidget 上,则可以更改播放的声音
//SoundEffectWidget 的构造函数执行特定任务以允许拖放
setAcceptDrops(true);
mSoundEffect.setVolume(1.0f);
mPlayButton->setSizePolicy(QSizePolicy::MinimumExpanding,
QSizePolicy::MinimumExpanding);
QFont font = mPlayButton->font();
font.setPointSize(15);
mPlayButton->setFont(font);
mFilenameLabel->setAlignment(Qt::AlignCenter);
QVBoxLayout* layout = new QVBoxLayout(this);
layout->addWidget(mPlayButton);
layout->addWidget(mFilenameLabel);
setLayout(layout);
//这个widget有一个名为 mPlayButton 的 QPushButton
connect(mPlayButton, &QPushButton::clicked,
this, &SoundEffectWidget::play);
}
void SoundEffectWidget::loadSound(const QUrl& url)
{
mSoundEffect.setSource(url);
mFilenameLabel->setText(url.fileName());
}
void SoundEffectWidget::play()
{
mSoundEffect.play();
emit soundPlayed(mId);
}
void SoundEffectWidget::triggerPlayButton()
{
//triggerPlayButton() 槽调用 QPushButton::animateClick() 函数
mPlayButton->animateClick();
}
void SoundEffectWidget::setId(int id)
{
mId = id;
}
void SoundEffectWidget::setName(const QString& name)
{
mPlayButton->setText(name);
}
//每次用户在小部件上拖动对象时,都会调用 dragEnterEvent() 函数
//在我们的例子中,我们只想允许拖放 MIME 类型的文件
void SoundEffectWidget::dragEnterEvent(QDragEnterEvent* event)
{
//“text/uri-list”(一个 URI 列表,可以是 file://、http:// 等 )
if (event->mimeData()->hasFormat("text/uri-list")) {
//在这种情况下,尽管我们可以调用 QDragEnterEvent::acceptProposedAction() 函数来通知我们接受此对象进行拖放。
event->acceptProposedAction();
}
}
void SoundEffectWidget::dropEvent(QDropEvent* event)
{
const QMimeData* mimeData = event->mimeData();
//第一步是健全性检查。 如果事件没有 URL,我们什么都不做。
//QMimeData::hasUrls() 函数仅对 MIME 类型“text/uri-text”返回 true
//请注意,用户可以一次拖放多个文件。 在我们的例子中,我们只处理第一个 URL
//您可以使用 MIME 类型检查该文件是否为 .wav 文件。 如果 MIME 类型是“audio/wav”,我们调用 loadSound() 函数,它更新分配给这个 SoundEffectWidget 的声音。
if (!mimeData->hasUrls()) {
return;
}
const QUrl url = mimeData->urls().first();
QMimeType mime = QMimeDatabase().mimeTypeForUrl(url);
if (mime.inherits("audio/wav")) {
loadSound(url);
}
}
//SoundEffectWidget 类提供了一个 getter 和一个 setter 来获取和设置成员变量 mTriggerKey。
Qt::Key SoundEffectWidget::triggerKey() const
{
return mTriggerKey;
}
void SoundEffectWidget::setTriggerKey(const Qt::Key& triggerKey)
{
mTriggerKey = triggerKey;
}
MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <memory>
#include <QMainWindow>
#include <QVector>
#include <QThread>
#include <QTimer>
#include "Track.h"
#include "Serializer.h"
class SoundEffectWidget;
class PlaybackWorker;
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
bool eventFilter(QObject* watched, QEvent* event) override;
void initSoundEffectWidgets();
private slots:
void playSoundEffect(int soundId);
void clearPlayback();
void stopPlayback();
void saveProject();
void loadProject();
private:
void updateState(const Track::State& state);
void startDisplayTimer();
void updateDisplayTime();
void stopDisplayTimer();
void startPlayback();
QString formatTime(long ms);
private:
Ui::MainWindow *ui;
Track mTrack;
QVector<SoundEffectWidget*> mSoundEffectWidgets;
PlaybackWorker* mPlaybackWorker;
QThread* mPlaybackThread;
QTimer mDisplayTimer;
std::unique_ptr<Serializer> mSerializer;
};
#endif // MAINWINDOW_H
MainWindow.cpp
#include "MainWindow.h"
#include "ui_MainWindow.h"
#include <QFileDialog>
#include <QKeyEvent>
#include "SoundEffectWidget.h"
#include "PlaybackWorker.h"
#include "JsonSerializer.h"
#include "XmlSerializer.h"
#include "BinarySerializer.h"
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow),
mTrack(),
mSoundEffectWidgets(),
mPlaybackWorker(nullptr),
mPlaybackThread(nullptr),
mSerializer(nullptr)
{
ui->setupUi(this);
setWindowTitle("Drum Machine");
//默认情况下,不调用 QObject::eventFilter() 函数。 要启用它并拦截这些事件,我们需要在 MainWindow 上安装一个事件过滤器:
//所以每次 MainWindow 接收到事件时,都会调用 MainWindow::eventFilter() 函数。
installEventFilter(this);
initSoundEffectWidgets();
mSerializer = std::make_unique<JsonSerializer>();
connect(ui->playButton, &QPushButton::clicked,
&mTrack, &Track::play);
connect(ui->recordButton, &QPushButton::clicked,
&mTrack, &Track::record);
connect(ui->stopButton, &QPushButton::clicked,
&mTrack, &Track::stop);
connect(ui->saveProjectAsAction, &QAction::triggered,
this, &MainWindow::saveProject);
connect(ui->loadProjectAction, &QAction::triggered,
this, &MainWindow::loadProject);
connect(ui->exitAction, &QAction::triggered,
this, &MainWindow::close);
connect(&mTrack, &Track::stateChanged,
this, &MainWindow::updateState);
connect(&mDisplayTimer, &QTimer::timeout,
this, &MainWindow::updateDisplayTime);
mTrack.stop();
}
MainWindow::~MainWindow()
{
delete ui;
clearPlayback();
}
void MainWindow::initSoundEffectWidgets()
{
mSoundEffectWidgets.append(ui->kickWidget);
mSoundEffectWidgets.append(ui->snareWidget);
mSoundEffectWidgets.append(ui->hihatWidget);
mSoundEffectWidgets.append(ui->crashWidget);
for (int i = 0; i < 4; ++i) {
SoundEffectWidget* widget = mSoundEffectWidgets[i];
widget->setId(i);
connect(widget, &SoundEffectWidget::soundPlayed,
&mTrack, &Track::addSoundEvent);
}
//MainWindow 类像这样初始化它的四个 SoundEffectWidget 的键setTriggerKey
ui->kickWidget->setName("Kick");
ui->kickWidget->setTriggerKey(Qt::Key_H);
ui->kickWidget->loadSound(QUrl("qrc:/sounds/kick.wav"));
ui->snareWidget->setName("Snare");
ui->snareWidget->setTriggerKey(Qt::Key_J);
ui->snareWidget->loadSound(QUrl("qrc:/sounds/snare.wav"));
ui->hihatWidget->setName("Hihat");
ui->hihatWidget->setTriggerKey(Qt::Key_K);
ui->hihatWidget->loadSound(QUrl("qrc:/sounds/hihat.wav"));
ui->crashWidget->setName("Crash");
ui->crashWidget->setTriggerKey(Qt::Key_L);
ui->crashWidget->loadSound(QUrl("qrc:/sounds/crash.wav"));
}
//此槽检索与 soundId 对应的 SoundEffectWidget 类。 然后,我们调用 triggerPlayButton(),该方法与您按下键盘上的触发键时调用的方法相同。
void MainWindow::playSoundEffect(int soundId)
{
mSoundEffectWidgets[soundId]->triggerPlayButton();
}
void MainWindow::updateState(const Track::State& state)
{
bool playEnabled = false;
bool recordEnabled = false;
bool stopEnabled = false;
QString statusText = "";
switch (state) {
case Track::State::STOPPED:
playEnabled = true;
recordEnabled = true;
stopEnabled = false;
statusText = "";
if (mTrack.previousState() == Track::State::PLAYING) {
stopPlayback();
}
stopDisplayTimer();
break;
case Track::State::PLAYING:
playEnabled = false;
recordEnabled = false;
stopEnabled = true;
statusText = "Playing...";
startDisplayTimer();
startPlayback();
break;
case Track::State::RECORDING:
playEnabled = false;
recordEnabled = false;
stopEnabled = true;
statusText = "Recording...";
startDisplayTimer();
break;
default:
break;
}
ui->playButton->setEnabled(playEnabled);
ui->recordButton->setEnabled(recordEnabled);
ui->stopButton->setEnabled(stopEnabled);
ui->statusBar->showMessage(statusText);
}
void MainWindow::startDisplayTimer()
{
mDisplayTimer.start(1000);
}
void MainWindow::updateDisplayTime()
{
QString elapsedTimeFormated = formatTime(mTrack.elapsedTime());
ui->currentTimeLabel->setText(elapsedTimeFormated);
if (mTrack.state() == Track::State::RECORDING) {
ui->totalTimeLabel->setText(elapsedTimeFormated);
}
}
void MainWindow::stopDisplayTimer()
{
mDisplayTimer.stop();
ui->currentTimeLabel->setText(formatTime(0));
ui->totalTimeLabel->setText(formatTime(mTrack.duration()));
}
void MainWindow::startPlayback()
{
//我们使用 clearPlayback() 函数清除当前播放,稍后将介绍该函数。
clearPlayback();
//构造了新的 QThread 和 PlaybackWorker
mPlaybackThread = new QThread();
mPlaybackWorker = new PlaybackWorker(mTrack);
//像往常一样,worker 然后被移动到它的专用线程
mPlaybackWorker->moveToThread(mPlaybackThread);
//因此,当 QThread 发出 started() 信号时,会调用 PlaybackWorker::play() 槽
connect(mPlaybackThread, &QThread::started,
mPlaybackWorker, &PlaybackWorker::play);
//我们不想担心 PlaybackWorker 内存。因此,当 QThread 结束并发送了 finished() 信号时,我们会调用 QObject::deleteLater() 槽,它会调度 worker 进行删除。
connect(mPlaybackThread, &QThread::finished,
mPlaybackWorker, &QObject::deleteLater);
//当 PlaybackWorker 类需要播放声音时,会发出 playSound() 信号并调用我们的 MainWindow:playSoundEffect() 槽。
connect(mPlaybackWorker, &PlaybackWorker::playSound,
this, &MainWindow::playSoundEffect);
//当 PlaybackWorker 类完成播放整个曲目时,最后一个连接会覆盖。发出 trackFinished() 信号,我们调用 Track::Stop() 槽。
connect(mPlaybackWorker, &PlaybackWorker::trackFinished,
&mTrack, &Track::stop);
//最后,线程以高优先级启动。请注意,某些操作系统(例如 Linux)不支持线程优先级。
mPlaybackThread->start(QThread::HighPriority);
}
QString MainWindow::formatTime(long ms)
{
int seconds = ms / 1000;
int minutes = (seconds / 60) % 60;
seconds = seconds % 60;
return QString()
.sprintf("%02d:%02d", minutes, seconds);
}
void MainWindow::stopPlayback()
{
//我们从我们的线程调用 PlaybackWorker 的 stop() 函数
//因为我们在 stop() 中使用了 QAtomicInteger,所以该函数是线程安全的,可以直接调用
mPlaybackWorker->stop();
//最后,我们调用我们的辅助函数 clearPlayback()
clearPlayback();
}
void MainWindow::saveProject()
{
QString filename = QFileDialog::getSaveFileName(
this,
"Save Drum Machine project",
QDir::homePath(),
"Drum Projects (*.dp)");
if (filename.isEmpty()) {
return;
}
mSerializer->save(mTrack, filename, "track");
ui->statusBar->showMessage("Project saved to " + filename);
}
void MainWindow::loadProject()
{
QString filename = QFileDialog::getOpenFileName(this,
"Load Drum Machine project",
QDir::homePath(),
"Drum Projects (*.dp)");
if (filename.isEmpty()) {
return;
}
mSerializer->load(mTrack, filename);
ui->statusBar->showMessage("Project loaded from " + filename);
mTrack.stop();
}
void MainWindow::clearPlayback()
{
// 如果线程有效,我们要求线程退出并等待 1 秒。 然后,我们将线程和工作线程设置为 nullptr。
if (mPlaybackThread) {
mPlaybackThread->quit();
mPlaybackThread->wait(1000);
mPlaybackThread = nullptr;
mPlaybackWorker = nullptr;
}
}
//所以每次 MainWindow 接收到事件时,都会调用 MainWindow::eventFilter() 函数。
bool MainWindow::eventFilter(QObject* watched, QEvent* event)
{
//首先要做的是检查 QEvent 类是否是 KeyPress 类型
//我们不关心其他事件类型
if (event->type() == QEvent::KeyPress) {
//将 QEvent 类转换为 QKeyEvent
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
//然后我们搜索按下的键是否属于 SoundEffectWidget 类
//如果 SoundEffectWidget 类对应于键
//,我们调用 SoundEffectWidget::triggerPlayButton() 函数并返回 true 以指示我们消耗了该事件并且它不能传播到其他类。
for(SoundEffectWidget* widget : mSoundEffectWidgets) {
if (keyEvent->key() == widget->triggerKey()) {
widget->triggerPlayButton();
return true;
}
}
}
//否则,我们调用 eventFilter() 的 QObject 类实现
return QObject::eventFilter(watched, event);
}