今天突然想到Microbit的音乐功能, 虽然只能输出蜂鸣声, 不过可以写点谱子来试试.
以后等女儿大了,我也得学点音乐和编曲软件, 不然没法教小孩用了.
一开始写了一个简单的转换函数, 后来发现这首歌的后半部分进行了转调, 所以打算编写一个完整的转调, 这个等后边再编写, 先是手工直接一个一个弄出来.
另外我发现microbit支持的高音不止到5, 更高的6和7也是可以的. 实际可以听的范围实在是太低, 基本也就在3-7之间.
- 播放代码
- 转换函数
- 最美的光
播放代码
曲子分为两部分, 第一部分是C大调版本, 第二部分是D大调版本, 剩下的过场还有转bE=C小调的方法, 待我一点点写一个转换函数.
from microbit import *
import music
music.set_tempo(bpm=130)
list1 = ['A:4', 'C:4', 'B3:4', 'A:4', 'G:4', 'B3:4', 'A3:4', 'G:4', 'F:4',
'A3:4', 'G3:4', 'F:4', 'E:8', 'C:4', 'C:1', 'D:1', 'E:1', 'G:1',
'A:12', 'B:2', 'C5:2', 'G:6', 'C:6', 'G:4', 'F:4', 'E:4', 'D:4',
'C:4', 'D:24', 'E:4', 'F:4', 'G:48', 'G:2', 'E:2', 'E:2', 'D:2',
'D:2', 'C:2', 'C:2', 'G3:2', 'G3:2', 'A3:2', 'C:2', 'D:2', 'E:8',
'A:2', 'G:2', 'G:2', 'E:2', 'E:2', 'D:2', 'C:2', 'D:2', 'A3:8',
'R:4', 'G3:2', 'A3:2', 'C:12', 'D:2', 'C:2', 'E:4', 'G:4', 'R:6',
'E:2', 'D:4', 'C:4', 'C:2', 'E:6', 'D:16', 'G:2', 'E:2', 'E:2',
'D:2', 'D:2', 'C:2', 'C:2', 'G3:2', 'G3:2', 'A3:2', 'C:2', 'D:2',
'E:8', 'A:2', 'G:2', 'G:2', 'E:2', 'E:2', 'D:2', 'C:2', 'D:2', 'A3:8',
'R:4', 'G3:2', 'A3:2', 'C:10', 'D:4', 'C:2', 'E:4', 'G:4', 'R:6', 'E:2',
'D:4', 'C:4', 'A3:4', 'C:4', 'C:16', 'R:8', 'R:8',
'E:2', 'G:4', 'E:2', 'G:4', 'E:4', 'A:16', 'E:2',
'G:4', 'E:2', 'G:4', 'E:4', 'A3:16', 'D:2', 'E:4', 'C:2', 'D:4', 'E:4',
'E:2', 'G:4', 'E:2', 'G:8', 'C5:4', 'B:4', 'A:4', 'G:2', 'A:18', 'D:2',
'E:4', 'C:2', 'D:4', 'E:4', 'E:2', 'G:4', 'E:2', 'G:8', 'C5:4', 'B:4', 'A:4', 'G:2', 'A:18','R:16']
list2 = ['F#:8', 'E:6', 'D:2', 'E:8', 'R:4', 'E:2', 'F#:2', 'G:4', 'F#:4', 'F#:4', 'E:4',
'F#:8', 'R:4', 'D:2', 'D:2', 'G:6', 'A:6', 'B:4', 'A:6', 'D:6', 'A:4', 'G:4',
'F#:4', 'E:4', 'D:2', 'E:18', 'F#:8', 'E:6', 'D:2', 'E:8', 'R:4', 'E:2', 'F#:2',
'G:4', 'F#:4', 'A#:4', 'C#5:4', 'E:4', 'D:2', 'D:2', 'R:4', 'D:2', 'D:2', 'G:6',
'A:6', 'B:4', 'A:6', 'D:6', 'A:4', 'G:4', 'F#:4', 'E:4', 'D:2', 'D:18']
def play_nin_gan():
for each in list1:
music.play(each)
for each in list2:
music.play(each)
for each in list2:
music.play(each)
while True:
if button_a.is_pressed():
play_nin_gan()
这里解释一下, 由于这首歌的最小单位是32分之一音符, 所以bpm设置的比较高, 然后长度2代表十六分之一音符, 4代表八分之一音符, 8代表四分音符, 16代表二分音符.
这个歌的简谱是每拍一个四分音符, 一小节有两拍. 不过钢琴曲是4/4的, 反正差不多了. 由于microbit不能演奏和弦, 所以表现力差了点, 以后学会了musescore大概就可以谱钢琴曲了.
转换函数
转换函数的思路来自音乐中「C 调」、「D 调」等的含义是什么? - 米叔的回答 - 知乎:
C → 无升降号→ 1 2 3 4 5 6 7 →a小调 G → 1升(#4)→ 5 6 7 1 2 3 #4 →e小调 D → 2升(#4 #1)→2 3 #4 5 6 7 #1 →b小调 A → 3升(#4 #1 #5)→ 6 7 #1 2 3 #4 #5 →#f小调 E → 4升(#4 #1 #5 #2)→ 3 #4 #5 6 7 #1 #2 →#c小调 B → 5升(#4 #1 #5 #2 #6)→ 7 #1 #2 3 #4 #5 #6 →#g小调 #F→ 6升(#4 #1 #5 #2 #6 #3)→ #4 #5 #6 7 #1 #2 #3 →#d小调 #C→ 7升(#4 #1 #5 #2 #6 #3 #7)→ #1 #2 #3 #4 #5 #6 #7 →#a小调 F → 1降(b7)→ 4 5 6 b7 1 2 3 →d小调 bB→ 2降(b7 b3)→ b7 1 2 b3 4 5 6 →g小调 bE→ 3降(b7 b3 b6)→ b3 4 5 b6 b7 1 2 →c小调 bA→ 4降(b7 b3 b6 b2)→ b6 b7 1 b2 b3 4 5 →f小调 bD→ 5降(b7 b3 b6 b2 b5)→ b2 b3 4 b5 b6 b7 1 →bb小调 bG→ 6降(b7 b3 b6 b2 b5 b1)→ b5 b6 b7 b1 b2 b3 4 →be小调 bC→ 7降(b7 b3 b6 b2 b5 b1 b4)→ b1 b2 b3 b4 b5 b6 b7 →ba小调
好在Microbit播放音高的时候先是音符号加上升降调号, 然后才是音高, 所以编写逻辑还是挺方便的.
这次就使用了Java来编写, 先粗略的编写了一个简单的版本, 使用了三个类, 一个类是Note
, 相当于简谱的一个音符, 一个类MicrobitMusicChanger
是用于将音符转换成Microbit的字符串.
还有一个工厂类, 用于启动几个不同调号的工厂进行翻译. 我试验了几段都没问题, 把代码贴出来, 只要不是特别刁钻的简谱, 只是在原音名上升降号, 应该没有问题.
class Note {
private int note;
private int length;
private int octave;
private int upOrDown = 0;
public Note(int note, int octave, int length, int upOrDown) {
this.note = note;
this.length = length;
this.octave = octave;
this.upOrDown = upOrDown;
}
public int getNote() {
return note;
}
public void setNote(int note) {
this.note = note;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getOctave() {
return octave;
}
public void setOctave(int octave) {
this.octave = octave;
}
public int getUpOrDown() {
return upOrDown;
}
public void setUpOrDown(int upOrDown) {
this.upOrDown = upOrDown;
}
@Override
public String toString() {
return ""Note{"" +
""note="" + note +
"", length="" + length +
"", octave="" + octave +
"", upOrDown="" + upOrDown +
'}';
}
}
Note类很简单, 其中保存四个int类型的数值, 第一个是音名也就是1234567和0, 0对应Microbit中的休止符'R'.
然后是MicrobitMusicChanger, 数据都存在这里, 之后感觉可以再整理一下, 把数据都拉出来.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class MicrobitMusicChanger {
//默认的声音水平, microbit默认就是4, 可以听得范围在2-7, 也就是从中央c可以升3个八度, 降2个八度, 一般足够了
private int octave = 4;
//使用哪个大调
private String tone = ""CTONE"";
//大调与1234567的对应关系
public static final String[] CTONE = {""C"", ""D"", ""E"", ""F"", ""G"", ""A"", ""B"", ""7""};
public static final String[] GTONE = {""G"", ""A"", ""B"", ""C"", ""D"", ""E"", ""F#"", ""3""};
public static final String[] DTONE = {""D"", ""E"", ""F#"", ""G"", ""A"", ""B"", ""C#"", ""6""};
public static final String[] ATONE = {""A"", ""B"", ""C#"", ""D"", ""E"", ""F#"", ""G#"", ""2""};
public static final String[] ETONE = {""E"", ""F#"", ""G#"", ""A"", ""B"", ""C#"", ""D#"", ""5""};
public static final String[] BTONE = {""B"", ""C#"", ""D#"", ""E"", ""F#"", ""G#"", ""A#"", ""1""};
public static final String[] FUTONE = {""F#"", ""G#"", ""A#"", ""B"", ""C#"", ""D#"", ""F"", ""4""};
public static final String[] CUTONE = {""C#"", ""D#"", ""F"", ""F#"", ""G#"", ""A#"", ""C"", ""6""};
public static final String[] FTONE = {""F"", ""G"", ""A"", ""Bb"", ""C"", ""D"", ""E"", ""4""};
public static final String[] bBTONE = {""Bb"", ""C"", ""D"", ""Eb"", ""F"", ""G"", ""A"", ""1""};
public static final String[] bETONE = {""Eb"", ""F"", ""G"", ""Ab"", ""Bb"", ""C"", ""D"", ""5""};
public static final String[] bATONE = {""Ab"", ""Bb"", ""C"", ""Db"", ""Eb"", ""F"", ""G"", ""2""};
public static final String[] bDTONE = {""Db"", ""Eb"", ""F"", ""Gb"", ""Ab"", ""Bb"", ""C"", ""6""};
public static final String[] bGTONE = {""Gb"", ""Ab"", ""Bb"", ""B"", ""Db"", ""Eb"", ""F"", ""4""};
public static final String[] bCTONE = {""B"", ""Db"", ""Eb"", ""E"", ""Gb"", ""Ab"", ""Bb"", ""1""};
//存储调号与字符串的对应关系, 以及单个音符升降的关系
private static final Map<String, String[]> notes = new HashMap<>();
private static final Map<String, String> noteToUpperNote = new HashMap<>();
private static final Map<String, String> noteToLowerNote = new HashMap<>();
static {
noteToUpperNote.put(""C"", ""C#"");
noteToUpperNote.put(""D"", ""D#"");
noteToUpperNote.put(""E"", ""F"");
noteToUpperNote.put(""F"", ""F#"");
noteToUpperNote.put(""G"", ""G#"");
noteToUpperNote.put(""A"", ""A#"");
noteToUpperNote.put(""B"", ""C"");
noteToUpperNote.put(""C#"", ""D"");
noteToUpperNote.put(""D#"", ""E"");
noteToUpperNote.put(""F#"", ""G"");
noteToUpperNote.put(""G#"", ""A"");
noteToUpperNote.put(""A#"", ""B"");
noteToLowerNote.put(""C"", ""B"");
noteToLowerNote.put(""D"", ""Db"");
noteToLowerNote.put(""E"", ""Eb"");
noteToLowerNote.put(""F"", ""E"");
noteToLowerNote.put(""G"", ""Gb"");
noteToLowerNote.put(""A"", ""Ab"");
noteToLowerNote.put(""B"", ""Bb"");
notes.put(""CTONE"", CTONE);
notes.put(""GTONE"", GTONE);
notes.put(""DTONE"", DTONE);
notes.put(""ATONE"", ATONE);
notes.put(""ETONE"", ETONE);
notes.put(""BTONE"", BTONE);
notes.put(""FUTONE"", FUTONE);
notes.put(""CUTONE"", CUTONE);
notes.put(""FTONE"", FTONE);
notes.put(""bBTONE"", bBTONE);
notes.put(""bETONE"", bETONE);
notes.put(""bATONE"", bATONE);
notes.put(""bDTONE"", bDTONE);
notes.put(""bGTONE"", bGTONE);
notes.put(""bCTONE"", bCTONE);
}
public String getTone() {
return tone;
}
public void setTone(String tone) {
this.tone = tone;
}
//核心方法, 读入一个aNote对象, 然后根据其四个属性进行操作
public String translateNumberedMusicalNotationToMicrobitMusicString(Note aNote) {
StringBuilder result = new StringBuilder();
//如果是0就返回休止符
String singleNote;
if (aNote.getNote() == 0) {
singleNote = ""R"";
} else {
singleNote = notes.get(tone)[aNote.getNote() - 1];
}
//升降号之后的音直接去查表
if (aNote.getUpOrDown() == 1) {
singleNote = noteToUpperNote.get(singleNote);
} else if (aNote.getUpOrDown() == -1) {
singleNote = noteToLowerNote.get(singleNote);
}
result.append(singleNote);
//这里要判断一下, 如果音符不等于0, 需要找到原来音符是否要抬高一个调号的位置, 然后加上合理的octave就可以了.
if (aNote.getNote() == 0) {
result.append(octave + aNote.getOctave());
} else {
int numberToRise = Integer.parseInt(notes.get(tone)[7]);
if (aNote.getNote() > numberToRise) {
result.append((octave + aNote.getOctave() + 1));
} else {
result.append(octave + aNote.getOctave());
}
}
result.append("":"" + aNote.getLength());
return ""'"" + result.toString() + ""'"";
}
}
MicrobitMusicChanger
的方便之处是默认设置翻译C大调, 但是可以更改采用哪个大调进行翻译.
最后一个类是NoteFactory
, 纯粹是为了不把MicrobitMusicChanger
暴露在外边而使用的类.
public class NoteFactory {
private MicrobitMusicChanger changer = new MicrobitMusicChanger();
private String tone = ""CTONE"";
public String getTone() {
return tone;
}
public void setTone(String tone) {
this.tone = tone;
}
public List<String> makeMusicFromArray(int[][] music) {
List<String> result = new ArrayList<>();
changer.setTone(this.tone);
for (int[] aSingleNotes : music) {
Note newNote = new Note(aSingleNotes[0], aSingleNotes[1], aSingleNotes[2], aSingleNotes[3]);
result.add(changer.translateNumberedMusicalNotationToMicrobitMusicString(newNote));
}
return result;
}
}
在写这篇博客的时候想到, 其实也不太需要包装一个Note类, 直接读入数组也是可以的. 也就懒得改了.
在实际使用的时候, 好比王菲的《人间》这首歌, 副歌之前的部分是C大调, 副歌是D大调, 因此可以启动两个Factory, 一个用于翻译C大调, 一个用于翻译D大调, 然后获取结果字符串, 就可以直接粘贴到Microbit的代码中使用:
public static void main(String[] args) {
//简谱前三小节
int[][] mu = {
{6, 0, 4, 0}, {1, 0, 4, 0}, {7, -1, 4, 0}, {6, 0, 4, 0},
{5, 0, 4, 0}, {7, -1, 4, 0}, {6, -1, 4, 0}, {5, 0, 4, 0},
{4, 0, 4, 0}, {6, -1, 4, 0}, {5, -1, 4, 0}, {4, 0, 4, 0},
{3, 0, 8, 0}, {1, 0, 4, 0}, {1, 0, 1, 0}, {2, 0, 1, 0}, {3, 0, 1, 0}, {5, 0, 1, 0},
{6, 0, 12, 0}, {7, 0, 2, 0}, {1, 1, 2, 0}
};
//使用默认转换C大调的工厂
NoteFactory factoryC = new NoteFactory();
List<String> music = factoryC.makeMusicFromArray(mu);
System.out.println(music);
//副歌 天上人间 如果真值得歌颂 也是因为有你 才会变得闹哄哄
int[][] muD = {
{3, 0, 8, 0}, {2, 0, 6, 0}, {1, 0, 2, 0},
{2, 0, 8, 0}, {0, 0, 4, 0}, {2, 0, 2, 0}, {3, 0, 2, 0},
{4, 0, 4, 0}, {3, 0, 4, 0}, {3, 0, 4, 0}, {2, 0, 4, 0},
{3, 0, 8, 0}, {0, 0, 4, 0}, {1, 0, 2, 0}, {1, 0, 2, 0},
{4, 0, 6, 0}, {5, 0, 6, 0}, {6, 0, 4, 0},
{5, 0, 6, 0}, {1, 0, 6, 0}, {5, 0, 4, 0},
{4, 0, 4, 0}, {3, 0, 4, 0}, {2, 0, 4, 0},{1, 0, 2, 0},{2, 0, 18, 0}
};
NoteFactory factoryD = new NoteFactory();
factoryD.setTone(""DTONE"");
List<String> music2 = factoryD.makeMusicFromArray(muD);
System.out.println(music2);
}
打印出来的结果是就不放了, 和刚才手工编写的是一样的.
这个程序目前除了某些地方比如从C降到第一级的B这种, 要注意手工调整一下高低八度, 其他都可以正常工作了
另外经过实验,mircobit不支持“Cb”这种实际上直接是个B音的写法,所以升降那里E和F互相升降,而不是用升E或者降F的写法。
最美的光
晚上回家给女儿听了, 女儿直接就跟着唱了起来, 然后问我能不能来一首最美的光, 没办法, 转换一遍:
lightlist1 = [
'G4:6', 'A4:2','G4:6','F4:1','E4:1',
'C4:2','B4:1','C5:1','D5:2','C5:1','B4:1','G4:8',
'R:2','B4:1','C5:1','D5:2','C5:1','B4:1','C5:4','C5:2','B4:1','G4:1',
'A4:3', 'G4:3','F4:1','E4:1','C4:2','G3:2','C4:2','C4:2',
]
lightlist2 =[
# 天上的星星 一眨一眨亮晶晶
'E4:2','D4:2','C4:2','E4:2',
'D4:6', 'C4:1', 'B3:1',
'A3:2','B3:2','C4:2','D4:2',
'E4:8',
# 我许下的愿望就像一颗水晶
'A3:2','B3:2','C4:2','D4:2',
'G3:4','E4:3','E4:1',
'D4:2','C4:2','A3:2','C4:2',
'D4:8',
# 汗水伴着我 一步一步往前闯
'E4:2','D4:2','C4:2','E4:2',
'D4:6', 'C4:1', 'B3:1',
'A3:2','B3:2','C4:2','D4:2',
'E4:8',
# 也常会有泪水在
'A3:2','B3:2','C4:2','A4:2',
'G4:4','C4:3','C4:1',
# 前进的路
'F4:2','E4:2','F4:2','A4:2',
# 上 心中的小梦想一闪
'G4:4', 'E4:2','D4:1','C4:1',
'C4:4', 'E4:2', 'G4:1','A4:1',
'G4:4', 'E4:2','D4:2',
# 一闪在发亮 穿越年少的迷
'C4:2','A3:2','C4:2','D4:2',
'E4:4','E4:2','E4:2',
'B4:2','B4:2','B4:2','G4:2',
#茫 我会变得更坚强心中
'C4:4', 'C4:2', 'B3:2',
'A3:2', 'B3:2', 'C4:2', 'D4:1', 'E4:1',
'D4:4', 'E4:2', 'D4:1', 'C4:1',
#的 小梦想 一天一天在成
'C4:4', 'E4:2', 'G4:1', 'A4:1',
'G4:4', 'E4:2', 'G4:2',
'A4:2', 'A4:2', 'G4:2', 'G4:1', 'A4:1',
# 长 天赐我一双翅膀 我会
'E4:4', 'E4:2', 'E4:2',
'B4:2', 'B4:2', 'B4:2', 'G4:2',
'C4:4', 'C4:2', 'B3:2',
# 看到那最美的光
'A3:2', 'B3:2', 'C4:2', 'D4:1', 'E4:1',
'D4:4', 'E4:4',
'C4:16'
]
def play_light():
music.set_tempo(bpm=120)
for each in lightlist1:
music.play(each)
music.set_tempo(bpm=60)
for each in lightlist2:
music.play(each)