【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-Si 1(Do),5(Sol) 基本音阶,基于 C 大调
0 表示休止符 0 无音,持续时间由时值决定
时值 默认:四分音符(1拍) 1 1拍,长度由 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:降半音 1b Do 降为 Dob,频率减少半音
小节线 | |

C++ 实现说明

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

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

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 
#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++ 编程实践中带来新的灵感和帮助。

你可能感兴趣的:(c++,开发语言)