【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
类,用于解析简谱字符串并播放音符。整个播放逻辑主要包含以下步骤:
- 解析 Token:将输入的简谱字符串按空格拆分,每个 token 根据前缀和后缀解析出音高、八度偏移、升降调及时值信息。
- 计算频率:根据音符的音高、八度调整和升降调计算出实际播放时的频率。
- 播放音符:利用 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++ 编程实践中带来新的灵感和帮助。