当前位置: 首页 > news >正文

【C++】自实现简谱播放

本文将介绍一套基于 ASCII 的简谱编码规则,并展示如何在 C++ 中利用这套规则实现简谱播放。该方案支持音高、时值、高低音、升降调、休止符以及小节线,确保编码规则既简洁又易于解析,同时还具备良好的扩展性。需要注意的是,此方案仅支持 Windows 操作系统。下面我将详细介绍这套规则和 C++ 实现的代码示例。


简谱编码规则

这套规则采用“音符 token”的概念,每个 token 都由以下部分组成:

  • 高低音前缀:用 ^ 表示升高一个八度,用 _ 表示降低一个八度。多个符号表示多个八度调整,如 ^^1 表示高两八度,__1 表示低两八度。
  • 音高:用 1-7 表示 Do 到 Si,数字 0 则表示休止符(无音)。
  • 升降调# 表示升半音,b 表示降半音。
  • 时值后缀:默认为四分音符(1拍),可通过后缀修改时值:
    • - 表示二分音符(2拍)
    • -- 表示全音符(4拍)
    • / 表示八分音符(1/2拍)
    • // 表示十六分音符(1/4拍)

每个音符 token 由 [高低音前缀][音高][升降调][时值后缀] 组成,不同 token 之间用空格分隔,小节线作为独立的 token,用于视觉分隔,不影响实际播放。

类别规则示例说明
音高1-7 表示 Do-Si1(Do),5(Sol)基本音阶,基于 C 大调
0 表示休止符0无音,持续时间由时值决定
时值默认:四分音符(1拍)11拍,长度由 BPM 决定
-:二分音符(2拍)1-2拍
--:全音符(4拍)1--4拍
/:八分音符(1/2拍)1/1/2拍
//:十六分音符(1/4拍)1//1/4拍
高低音^ 前缀:升高一个八度^1高音 Do,频率翻倍
_ 前缀:降低一个八度_1低音 Do,频率减半
多个 ^_:多个八度^^1(高两八度),__1(低两八度)每增加一个,频率乘以 2 或除以 2
升降调#:升半音1#Do 升为 Do#,频率增加半音
b:降半音1bDo 降为 Dob,频率减少半音
小节线||

C++ 实现说明

下面是 C++ 的完整实现代码。代码中定义了一个 NotePlayer 类,用于解析简谱字符串并播放音符。整个播放逻辑主要包含以下步骤:

  1. 解析 Token:将输入的简谱字符串按空格拆分,每个 token 根据前缀和后缀解析出音高、八度偏移、升降调及时值信息。
  2. 计算频率:根据音符的音高、八度调整和升降调计算出实际播放时的频率。
  3. 播放音符:利用 Windows 的 Beep 函数播放对应频率和时值的音符;若遇休止符则通过 Sleep 实现等待。
#pragma once
#include <iostream>
#include <sstream>
#include <string>
#include <cmath>
#include <thread>
#include <windows.h>

class NotePlayer {
public:
    NotePlayer(int bpm = 120) : bpm(bpm), quarterDuration(60000 / bpm), isPlaying(false) {}
    
    // 播放简谱,async = true 为异步播放
    void play(const std::string& song, bool async = false) {
        if (isPlaying) return;
        isPlaying = true;
        auto playLogic = [this, song]() {
            std::istringstream iss(song);
            std::string token;
            while (iss >> token && isPlaying) if (token != "|") playNote(parseToken(token));
            isPlaying = false;
        };
        if (async) { playThread = std::thread(playLogic); playThread.detach(); } else playLogic();
    }
    
    // 停止播放(仅对异步有效)
    void stop() { isPlaying = false; }
    
private:
    int bpm, quarterDuration;
    std::thread playThread;
    bool isPlaying;
    
    struct Note { int frequency = 0, duration = 0; };
    
    // 播放单个音符
    void playNote(const Note& note) {
        if (note.duration <= 0) return;
        if (note.frequency > 0) Beep(note.frequency, note.duration);
        else Sleep(note.duration);
    }
    
    // 解析简谱 token
    Note parseToken(const std::string& token) {
        Note note; size_t pos = 0; int octaveOffset = 0;
        while (pos < token.size() && (token[pos] == '^' || token[pos] == '_')) octaveOffset += (token[pos++] == '^') ? 1 : -1;
        if (pos >= token.size() || token[pos] < '0' || token[pos] > '7') return note;
        char pitch = token[pos++], accidental = ' ';
        if (pos < token.size() && (token[pos] == '#' || token[pos] == 'b')) accidental = token[pos++];
        double multiplier = (pos < token.size() && token.substr(pos) == "--") ? 4.0 : (pos < token.size() && token.substr(pos) == "-") ? 2.0 : (pos < token.size() && token.substr(pos) == "//") ? 0.25 : (pos < token.size() && token.substr(pos) == "/") ? 0.5 : 1.0;
        note.duration = static_cast<int>(quarterDuration * multiplier);
        note.frequency = calculateFrequency(pitch, octaveOffset, accidental);
        return note;
    }
    
    // 计算音符频率
    int calculateFrequency(char pitch, int octaveOffset, char accidental) {
        if (pitch == '0') return 0;
        int semitoneOffset = (pitch == '1') ? 0 : (pitch == '2') ? 2 : (pitch == '3') ? 4 : (pitch == '4') ? 5 : (pitch == '5') ? 7 : (pitch == '6') ? 9 : 11;
        if (accidental == '#') semitoneOffset += 1; else if (accidental == 'b') semitoneOffset -= 1;
        return static_cast<int>(261.63 * std::pow(2.0, octaveOffset) * std::pow(2.0, semitoneOffset / 12.0) + 0.5);
    }
};

使用示例

以下是一个简单的使用示例,演示如何调用 NotePlayer 类来播放一段简谱。示例中使用了斗地主主题音乐作为演示内容。

#include <iostream>
#include "NotePlayer.hpp"

int main() {
    // 示例:斗地主主题音乐(佚名)
    std::string song = R"(
        3 3/ 2/ | 1 1/ _6/ | 2/ 3/ 2/ 3/ | _5- 
        _6 _6/ _5/ | _6 1 | 5/ 6/ 3/ 5/ | 2- 
        3 3/ 2/ | 3 5 | 6/ 6/ 6/ ^1/ | 6 5/ 3/ 
        2 2/ 3/ | 5 _5 | 2/ 3/ 2/ 3/ | 1- 
        3 3/ 2/ | 3 5 | 6/ ^1/ 6/ 5/ | 6 5/ 3/ 
        2 2/ 3/ | 5 _5 | 2/ 3/ 2/ 3/ | 1- 
        2/ 2/ 2/ 3/ | 5 5/ 6/ | ^1 6 | ^1- 
    )";

    NotePlayer player(120); // BPM 120
    std::cout << "Playing the simplified score..." << std::endl;
    player.play(song);

    return 0;
}

总结

本文介绍了如何利用 C++ 结合自定义的简谱编码规则实现简谱播放。通过简单的 ASCII 编码方式,不仅使音乐表示更加直观,同时也保证了解析的高效和扩展性。希望这篇文章能给大家在音频处理和 C++ 编程实践中带来新的灵感和帮助。

相关文章:

  • ESP-IDF中调用xEventGroupWaitBits函数失效问题的分析(1)
  • 碰一碰发视频网页版本开发的源码搭建指南
  • 三、FFmpeg学习笔记
  • 26--DHCP Snooping:网络世界的“房产中介资格认证系统“
  • 解锁健康密码,踏上养生旅程
  • YOLOV8 训练姿态检测模型
  • linux权限
  • 【实战】渗透测试下的传输命令
  • Linux安装Ubuntu24.04系统 并安装配置Nvidia 4090 显卡驱动
  • PTS-G3K13M RF Generator 3kW / 13MHz User’s Manual 手侧
  • Redis 6.2.6 生产环境单机配置详解redis.conf
  • 循环神经网络 - 简单循环网络
  • 正则表达式最小生成树算法题
  • 安全编码课程 实验5 动态内存(3)
  • Linux进程间通信:无名管道与有名管道的原理与实践
  • 4月1日工作日志
  • 用python编写poc的流程
  • 文件系统简介
  • web前端开发-HTML-CSS(0-1)
  • Python入门(4):函数
  • 哈马斯与以色列在多哈举行新一轮加沙停火谈判
  • 女排奥运冠军宋妮娜:青少年保持身心健康才能走得更远
  • 雅典卫城上空现“巨鞋”形状无人机群,希腊下令彻查
  • 贵州省委军民融合发展委员会办公室副主任李刚接受审查调查
  • 四川内江警方通报一起持刀伤人致死案:因车辆停放引起,嫌犯被抓获
  • 警方通报男子广州南站持刀伤人:造成1人受伤,嫌疑人被控制