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:已播放声音的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);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值