Mastering Qt 5 学习笔记-drum-machine

2024-01-08 22:10

本文主要是介绍Mastering Qt 5 学习笔记-drum-machine,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

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:已播放声音的IDint 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:已播放声音的IDint 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:已播放声音的IDint 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 并将其添加到 mSoundEventsvoid addSoundEvent(int soundEventId);private://此函数重置 Track 的内容:它清除 mSoundEvents 并将 mDuration 设置为 0void 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 并启动 mTimersetState(State::PLAYING);mTimer.start();
}void Track::record()
{clear();setState(State::RECORDING);mTimer.start();
}void Track::stop()
{//它首先清除数据,将状态设置为 RECORDING,并启动 Timer//如果我们停止在 RECORDING 状态,则更新 mDurationif (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 类,并初始化一个 soundEventIndexmIsPlaying.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() 函数会生成一个 QVariantMapQJsonDocument 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 或 QVariantMapserializable.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 对象//默认情况下,编写器将生成一个紧凑的 XMLQXmlStreamWriter stream(&file);//您可以使用 QXmlStreamWriter::setAutoFormatting() 函数生成漂亮的 XML。stream.setAutoFormatting(true);//QXmlStreamWriter::writeStartDocument() 函数写入 XML 版本和编码stream.writeStartDocument();//我们使用 writeVariantToStream() 函数在 XML 流中编写 QVariantwriteVariantToStream(rootName, serializable.toVariant(), stream);stream.writeEndDocument();//最后,我们结束文档并关闭 QFilefile.close();
}void XmlSerializer::load(Serializable& serializable, const QString& filepath)
{//首先要做的是创建一个带有源文件路径的 QFileQFile file(filepath);file.open(QFile::ReadOnly);//我们用 QFile 构造一个 QXmlStreamReaderQXmlStreamReader 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 是一个容器,但这次是 QVariantMapQVariantMap map = variant.toMap();QMapIterator<QString, QVariant> i(map);//我们为遍历的每个元素调用 writeVariantToStream()。//标签名称很重要,因为这是一个mapwhile (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();//这个函数的作用是创建一个QVariantQVariant 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 类型是一个 QStringQVariant variant(dataString);//所以我们使用 QVariant::convert() 函数将 QVariant 转换为真实类型(int、qlonglong 等)variant.convert(QVariant::nameToType(typeString.toStdString().c_str()));return variant;
}QVariant XmlSerializer::readVariantListFromStream(QXmlStreamReader& stream)
{//我们知道流元素包含一个数组//所以,这个函数创建并返回一个 QVariantListQVariantList 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 的 QPushButtonconnect(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_OBJECTpublic: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 的键setTriggerKeyui->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 和 PlaybackWorkermPlaybackThread = 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 类转换为 QKeyEventQKeyEvent* 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);
}

这篇关于Mastering Qt 5 学习笔记-drum-machine的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/584996

相关文章

HarmonyOS学习(七)——UI(五)常用布局总结

自适应布局 1.1、线性布局(LinearLayout) 通过线性容器Row和Column实现线性布局。Column容器内的子组件按照垂直方向排列,Row组件中的子组件按照水平方向排列。 属性说明space通过space参数设置主轴上子组件的间距,达到各子组件在排列上的等间距效果alignItems设置子组件在交叉轴上的对齐方式,且在各类尺寸屏幕上表现一致,其中交叉轴为垂直时,取值为Vert

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

学习hash总结

2014/1/29/   最近刚开始学hash,名字很陌生,但是hash的思想却很熟悉,以前早就做过此类的题,但是不知道这就是hash思想而已,说白了hash就是一个映射,往往灵活利用数组的下标来实现算法,hash的作用:1、判重;2、统计次数;

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【机器学习】高斯过程的基本概念和应用领域以及在python中的实例

引言 高斯过程(Gaussian Process,简称GP)是一种概率模型,用于描述一组随机变量的联合概率分布,其中任何一个有限维度的子集都具有高斯分布 文章目录 引言一、高斯过程1.1 基本定义1.1.1 随机过程1.1.2 高斯分布 1.2 高斯过程的特性1.2.1 联合高斯性1.2.2 均值函数1.2.3 协方差函数(或核函数) 1.3 核函数1.4 高斯过程回归(Gauss

【学习笔记】 陈强-机器学习-Python-Ch15 人工神经网络(1)sklearn

系列文章目录 监督学习:参数方法 【学习笔记】 陈强-机器学习-Python-Ch4 线性回归 【学习笔记】 陈强-机器学习-Python-Ch5 逻辑回归 【课后题练习】 陈强-机器学习-Python-Ch5 逻辑回归(SAheart.csv) 【学习笔记】 陈强-机器学习-Python-Ch6 多项逻辑回归 【学习笔记 及 课后题练习】 陈强-机器学习-Python-Ch7 判别分析 【学

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

线性代数|机器学习-P36在图中找聚类

文章目录 1. 常见图结构2. 谱聚类 感觉后面几节课的内容跨越太大,需要补充太多的知识点,教授讲得内容跨越较大,一般一节课的内容是书本上的一章节内容,所以看视频比较吃力,需要先预习课本内容后才能够很好的理解教授讲解的知识点。 1. 常见图结构 假设我们有如下图结构: Adjacency Matrix:行和列表示的是节点的位置,A[i,j]表示的第 i 个节点和第 j 个