今天突然想到Microbit的音乐功能, 虽然只能输出蜂鸣声, 不过可以写点谱子来试试.
以后等女儿大了,我也得学点音乐和编曲软件, 不然没法教小孩用了.

一开始写了一个简单的转换函数, 后来发现这首歌的后半部分进行了转调, 所以打算编写一个完整的转调, 这个等后边再编写, 先是手工直接一个一个弄出来.

另外我发现microbit支持的高音不止到5, 更高的6和7也是可以的. 实际可以听的范围2实在是太低, 基本也就在3-7之间.

  1. 播放代码
  2. 转换函数
  3. 最美的光

播放代码

曲子分为两部分, 第一部分是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)