Skip to content

Commit ff34248

Browse files
committed
changed in-group sequence seperator from "|" to ",", and "|" becomes a bar line
1 parent 7f7d7ae commit ff34248

14 files changed

+218
-71
lines changed

.vscode/launch.json

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
55
"version": "0.2.0",
66
"configurations": [
7+
{
8+
"type": "node",
9+
"request": "launch",
10+
"name": "bin",
11+
"program": "${workspaceFolder}/bin/quick-midi",
12+
"args": [
13+
"r"
14+
]
15+
},
716
{
817
"type": "node",
918
"request": "launch",

README.md

+11-6
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Happy new year
3232
1
3333
```
3434

35+
More examples are in the `examples/` directory.
36+
3537
### Structure
3638
The input consists several tracks, each track consists several voices, and the voices are sequences of sound notes. Tracks and
3739
voices are indicated by directives, specifying their names.
@@ -46,13 +48,16 @@ voices are indicated by directives, specifying their names.
4648
4749
...
4850
```
49-
When a track or voice appears for the first time, they'll be created, and when they appear again later in the input, the content
50-
will be appended to the existing one. This allows one to seperate a track or voices into several parts, making the input more
51-
readable.
51+
Directives are started by `\`, as TeX does. When a track or voice appears for the first time, they'll be created, and when they
52+
appear again later in the input, the content will be appended to the existing one. This allows one to seperate a track or voices
53+
into several parts, making the input more readable.
5254

53-
For simplicity, the first track of the input and first voices of a track can appear directly, without `\v` or `\track` directive,
54-
in which case their names will be assigned as `Track 1` and `1` respectively. Thus a single sequence input is legal.
55+
For simplicity, the beginning directives, i.e. `\track` and `\v`, of the first track of the input and first voices of a track can be omitted, in which case their names will be assigned as `Track 1` and `1` respectively. Thus a single sequence input is legal, as the above examples do.
5556

5657
### Note sequences and modifiers
5758
Basically, the note sequence consists of sound notes and directives. Each notes could be followed by some modifiers, modifying
58-
their tone and durations.
59+
their tone and durations. A **note** is a number 0 to 7, where `0` represents musical rest, 1 to 7 corresponds to musical notes
60+
in diatonic major scale. As numbered musical notation does, the notation uses a movable Do system, in which case the pitch of the
61+
note `1` is **C4** by default, and can be redefined by directives (see below). With no modifiers, all notes are quater notes.
62+
63+
Modifiers

bin/cli.js

+16-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const usage =
99
1010
Options:
1111
-o <output file> Specify output file, default is a.mid;
12-
-d Dump MIDI file to terminal;
12+
-d Dump MIDI file (events) to terminal;
1313
-h, --help Display this help message and quit.
1414
`;
1515

@@ -78,6 +78,11 @@ function parseArgs(args){
7878
return opts;
7979
}
8080

81+
function printErrMsg(lines, msg){
82+
console.log(msg.msg);
83+
84+
}
85+
8186
async function main(args){
8287
var opt = parseArgs(args);
8388
if (opt.errMsg){
@@ -90,19 +95,25 @@ async function main(args){
9095
return 0;
9196
}
9297
var ctx = qmidi.createContext();
93-
var midiFile = ctx.parse(opt.isFile ? await readFile(opt.input, 'utf-8') : opt.input);
98+
var input = opt.isFile ? await readFile(opt.input, 'utf-8') : opt.input;
99+
var lines = input.split(/\n|\r\n|\r/);
100+
var midiFile = ctx.parse(input);
94101
var errors = ctx.getErrors();
95-
if (errors.length > 0){
96-
for (var e of errors){
97-
console.log(e.msg);
102+
if (ctx.hasError()){
103+
for (var e of ctx.getPrintedErrors({ getLine: i => lines[i - 1] })){
104+
console.log(e);
98105
}
106+
return -1;
99107
}
100108
else {
101109
if (opt.dump){
102110
for (var line of midiFile.dump(true)){
103111
console.log(line);
104112
}
105113
}
114+
if (midiFile.isEmpty()){
115+
console.log('Warning: creating empty MIDI file.');
116+
}
106117
var midiData = qmidi.saveMidiFile(midiFile);
107118
await writeFile(opt.outFile, Buffer.from(midiData));
108119
return 0;

examples/ballade-pour-adeline.txt

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
% Ballade pour Adeline, par Richard
22
\bpm{120}
3-
\v{1} {151'2' 151'2'}_ {151'2' 151'2'}_ {153'5 153'5}_ {153'5 153'5}_
4-
\v{2} 0--- ---- ---- ----
3+
\v{1} {151'2' 151'2'}_ {151'2' 151'2'}_ {153'5 153'5}_ {153'5 153'5}_
4+
\v{2} \vel{70} 0--- ---- 1.--- ----
55

66
\v{1} 3'-3'- --3'*4'_ 4'-4'_--- -4'_{4'4'4'4'4'}_5'_ 5'-5'- --5'*6'_ 3'-3'- 0---
77
\v{2} 1.5.35. 35.35. 2.6.46. 46.46. 5.272 7272 1.5.35. 35.35..
88

99
\v{1} 3'-3'_--- -{3'3'3'3'3'3'}_4'_
10-
\v{2} 1..5.35. {1.{5.|3}{5.|3}{5.|3}{5.|3}{5.|3}{5.|3}{5.|3}}_
10+
\v{2} 1..5.35. {1.{5.,3}{5.,3}{5.,3}{5.,3}{5.,3}{5.,3}{5.,3}}_
1111

1212
\v{1} 4'-4'_--- -4'_{4'4'4'4'4'}_5'_
13-
\v{2} 2.6.46. {2.{6.|4}{6.|4}{6.|4}{6.|4}{6.|4}{6.|4}{6.|4}}_
13+
\v{2} 2.6.46. {2.{6.,4}{6.,4}{6.,4}{6.,4}{6.,4}{6.,4}{6.,4}}_
1414

1515
\v{1} 5'-5'- --5'*6'_ 3'-3'- 0---
1616
\v{2} 5.272 72'0- 1.5.35. 1.7..6..6..

examples/canon-in-c.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
\def\repeat$1$2{$1$2$1}
44

5-
{3217.6.5.6.7. {3'|5}{2'|5}{1'|3}{7|3}{6|1}{5|1}{6|1}{7|3}}---
5+
{3217.6.5.6.7. {3',5}{2',5}{1',3}{7,3}{6,1}{5,1}{6,1}{7,3}}---
66

77
1'71'3 5-7- 1'-3'- 5'3'5'6' 4'3'2'4' 3'2'1'7 641'- 731'7 1'71'4 5-7- 1'-3'1' 5'3'5'6' 4'3'2'4' 3'2'1'7 6541' 1'*5_1'2'
88

examples/dream-wedding.txt

+38-17
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,50 @@
11
% par Richard, my favourite
22
% USE MONOSPACED FONT TO VIEW THIS FILE !!!
33

4-
\def\ham$1{$1|$1'}
4+
\def\ham$1{$1,$1'}
55

66
\major{Bb}\bpm{73}
7-
\v{1} 0-- {0--6'}__ {6'7'7'1''1''7'7'6' 6'3'3'1'1'665'}__ \times34 {5'4'4'3'4'5'}__4'*
8-
\v{2} \vel{70} {6..3.13. 13.13.}_ {6..3.13. 13.13.}_ {2.6.46. 46.}_
7+
\v{1} 0-- {0--6'}__ | {6'7'7'1''1''7'7'6' 6'3'3'1'1'665'}__ | \times34 {5'4'4'3'4'5'}__4'* |
8+
\v{2} \vel{70} {6..3.13. 13.13.}_ | {6..3.13. 13.13.}_ | {2.6.46. 46.}_ |
99

10-
\v{1} \times78 {04 45 56 67 75 52 24}'__ \times68 {43 32 34}'__ 3'*
11-
\v{2} {5..2. 7. 2. 7. 2. 7.}_ {1.5.3}_ 3.*
10+
\v{1} \times78 {04 45 56 67 75 52 24}'__ | \times68 {43 32 34}'__ 3'* |
11+
\v{2} {5..2. 7. 2. 7. 2. 7.}_ | {1.5.3}_ 3.* |
1212

13-
\v{1} \times{12}8 {3'{61'3'2'}_ 3'{61'3'2'}_ 3'{61'4'3'}_ 4'{61'4'3'}_}_ \times98 {4'{4'3'4'4'#}_ 5'{5'6'5'6'}_}_ 3'*
14-
\v{2} {6..3.1 3.13. 6..3.1 2.6.4 }_ {6.46. 5..2.7. 1.1{7.|7..}}_
13+
\v{1} \times{12}8 {3'{61'3'2'}_ 3'{61'3'2'}_ 3'{61'4'3'}_ 4'{61'4'3'}_}_ | \times98 {4'{4'3'4'4'#}_ 5'{5'6'5'6'}_}_ 3'*
14+
\v{2} {6..3.1 3.13. 6..3.1 2.6.4 }_ | {6.46. 5..2.7. 1.1{7.,7..}}_
1515

16-
\v{1} \times{12}8 {{3'{61'3'2'}_ 3'{61'3'2'}_ 3'{61'4'3'}_ 4'{61'4'3'}_}_ \times98 {4'{4'3'4'4'#}_ 5'{5'6'5'6'}_}_ 3'*}'
17-
\v{2} {6..3.1 3.13. 6..3.1 2.6.4 }_ {6.46. 5..2.7. 1.{1|1.}{7.|7..}}_
16+
\v{1} \times{12}8 {{3'{61'3'2'}_ 3'{61'3'2'}_ 3'{61'4'3'}_ 4'{61'4'3'}_}_ | \times98 {4'{4'3'4'4'#}_ 5'{5'6'5'6'}_}_ 3'*}'
17+
\v{2} {6..3.1 3.13. 6..3.1 2.6.4 }_ | {6.46. 5..2.7. 1.{1,1.}{7.,7..}}_
1818

19-
\v{1} \times{12}8 {1'*{334}_ 4*{276}_ 7*{223}_ 3{1165}_}'_ \times98 {6*{112}_ 2*{7.32}_}'_ 3'*
20-
\v{2} {{6..|6.}3.1 2.6.4 5..2.7. 1.5.7..}_ {6..3.1 7..4.2 3 {4.5.|4..5..}#}_
19+
\v{1} \times{12}8 {1'*{334}_ 4*{276}_ 7*{223}_ 3{1165}_}'_ | \times98 {6*{112}_ 2*{7.32}_}'_ 3'*
20+
\v{2} {{6..,6.}3.1 2.6.4 5..2.7. 1.5.7..}_ | {6..3.1 7..4.2 3 {4.5.,4..5..}#}_
2121

22-
\v{1} \times{12}8 {1'*{1'1'2'}_ 2'*{1'76}_ 5*{565}_ }'_3'* {1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_}'_6'*
23-
\v{2} {{6..|6.}3.1 2.6.4 5..2.7. 1.1{7.|7..}}_ {{6..|6.}3.1 2.6.4 5..2.7. 6..3.1 }_
22+
\v{1} \times{12}8 {1'*{1'1'2'}_ 2'*{1'76}_ 5*{565}_ }'_3'* | {1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_}'_6'* |
23+
\v{2} {{6..,6.}3.1 2.6.4 5..2.7. 1.1{7.,7..}}_ | {{6..,6.}3.1 2.6.4 5..2.7. 6..3.1 }_ |
2424

25-
\v{1} \ham{{1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_ }'_3'*}
26-
\v{2} {{6..|6.}3.1 2.6.4 5..2.7. 1.1{7.|7..}}_
25+
\v{1} \ham{{1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_ }'_3'*} |
26+
\v{2} {{6..,6.}3.1 2.6.4 5..2.7. 1.1{7.,7..}}_ |
2727

28-
\v{1} \times98 \ham{{1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_}'_} {6'|6''}--
29-
\v{2} {{6..|6.}3.1 2.6.4 5..2.7. }_ {6..3.1361'}_
28+
\v{1} \times98 \ham{{1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_}'_} {6',6''}-- |
29+
\v{2} {{6..,6.}3.1 2.6.4 5..2.7. }_ {6..3.1361'}_ |
30+
31+
\v{1} \times{12}8 {3'{61'3'2'}_ 3'{61'3'2'}_ 3'{61'4'3'}_ 4'{61'4'3'}_}_ | \times98 {4'{4'3'4'4'#}_ 5'{5'6'5'6'}_}_ 3'*
32+
\v{2} {6..3.1 3.13. 6..3.1 2.6.4 }_ | {6.46. 5..2.7. 1.1{7.,7..}}_
33+
34+
\v{1} \times{12}8 {{3'{61'3'2'}_ 3'{61'3'2'}_ 3'{61'4'3'}_ 4'{61'4'3'}_}_ | \times98 {4'{4'3'4'4'#}_ 5'{5'6'5'6'}_}_ 3'*}'
35+
\v{2} {6..3.1 3.13. 6..3.1 2.6.4 }_ | {6.46. 5..2.7. 1.{1,1.}{7.,7..}}_
36+
37+
\v{1} \times{12}8 {1'*{1'1'2'}_ 2'*{1'76}_ 5*{565}_ }'_3'* | {1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_}'_6'*
38+
\v{2} {{6..,6.}3.1 2.6.4 5..2.7. 1.1{7.,7..}}_ | {{6..,6.}3.1 2.6.4 5..2.7. 6..3.1 }_
39+
40+
\v{1} \ham{{1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_ }'_3'*} |
41+
\v{2} {{6..,6.}3.1 2.6.4 5..2.7. 1.1{7.,7..}}_ |
42+
43+
\v{1} \times98 \ham{{1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_}'_} | \times68 {6',6''}--
44+
\v{2} {{6..,6.}3.1 2.6.4 5..2.7. }_ | {6..3.1 361'}_
45+
46+
\v{1} \ham{{1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_ }'_3'*} |
47+
\v{2} {{6..,6.}3.1 2.6.4 5..2.7. 1.1{7.,7..}}_ |
48+
49+
\v{1} \times98 \ham{{1'{1'1'1'2'}_ 2'*{1'76}_ 5*{565}_}'_} | {6',6''}_-------- 6'*
50+
\v{2} {{6..,6.}3.1 2.6.4 5..2.7. }_ | {6..3.1 361'}_

examples/ki.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
\major{Ab}
2+
\v{1} {5.12}''_ | {23}''3''- |0--- | {1232}''
3+
\v{2} \vel{70} {000}_ | 15.25. |1--- | 375'7 | 3---

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "quick-midi",
33
"version": "1.0.0",
4-
"description": "Creating midi files from numbered musical notation (简谱)",
4+
"description": "Create MIDI files from numbered musical notation",
55
"main": "index.js",
66
"bin": "bin/quick-midi",
77
"scripts": {
@@ -11,6 +11,7 @@
1111
"music",
1212
"sound",
1313
"midi",
14+
"MIDI",
1415
"generator",
1516
"parser"
1617
],

src/Parser.ts

+41-21
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,14 @@ export function createParser(eReporter: ErrorReporter): Parser{
352352
let { tracks, file } = parseTop();
353353

354354
for (let name in tracks){
355-
file.tracks.push(convertTrack(tracks[name]));
355+
let track = convertTrack(file, tracks[name]);
356+
track.events.length > 0 && file.tracks.push(track);
356357
}
357358

358359
return file;
359360
}
360361

361-
function convertTrack(track: NodeTrack): Track {
362+
function convertTrack(file: MidiFile, track: NodeTrack): Track {
362363
let list = new EventNodeList();
363364
for (let l in track.voices){
364365
list.appendChild(track.voices[l].events, true);
@@ -367,7 +368,7 @@ export function createParser(eReporter: ErrorReporter): Parser{
367368
name: track.name,
368369
volume: track.volume,
369370
instrument: track.instrument,
370-
events: pollAllEvents(createOverlappedEventMerger(createNoteQueue(list)))
371+
events: pollAllEvents(createOverlappedEventMerger(createNoteQueue(list, file.division)))
371372
};
372373
}
373374

@@ -411,7 +412,7 @@ export function createParser(eReporter: ErrorReporter): Parser{
411412
if (n) {
412413
let tempo = Number(n.text);
413414
if (tempo > 0xffffff) {
414-
eReporter.complationError('Tempo value too large, should be less than 0xffffff', n);
415+
eReporter.complationError('Tempo value too large, should be less than 16777215 (0xffffff)', n);
415416
return -1;
416417
}
417418
else
@@ -428,7 +429,7 @@ export function createParser(eReporter: ErrorReporter): Parser{
428429
if (regNum.test(n.text)){
429430
let tempo = 60000000 / Number(n.text);
430431
if (tempo > 0xffffff) {
431-
eReporter.complationError(`Tempo value too large (${tempo}), should be less than 0xffffff`, n);
432+
eReporter.complationError(`Tempo value too large (${tempo}), should be less than 16777215 (0xffffff)`, n);
432433
return -1;
433434
}
434435
else
@@ -494,6 +495,19 @@ export function createParser(eReporter: ErrorReporter): Parser{
494495
file.timesig = s;
495496
}
496497
}
498+
else if (isMacro(tk, 'div')){
499+
next();
500+
let t = readNumber();
501+
if (t){
502+
let div = Number(t.text);
503+
if (div >= 0x7fff){
504+
eReporter.complationError('Division value too large, should be no more than 32767 (0x7fff)', t);
505+
}
506+
else {
507+
file.division = div;
508+
}
509+
}
510+
}
497511
else
498512
break;
499513
tk = peek();
@@ -557,15 +571,15 @@ export function createParser(eReporter: ErrorReporter): Parser{
557571
}
558572

559573
/**
560-
* '{' Sequence ( '|' Sequence )* '}'
574+
* '{' Sequence ( ',' Sequence )* '}'
561575
*/
562576
function parseGroup(): EventNodeList {
563577
next();
564578
enterScope();
565579
let list = parseSequence();
566580
leaveScope();
567581
let tk = peek();
568-
while (tk.type !== TokenType.EOF && tk.text === '|'){
582+
while (tk.type !== TokenType.EOF && tk.text === ','){
569583
next();
570584
enterScope();
571585
list.appendChild(parseSequence());
@@ -581,7 +595,7 @@ export function createParser(eReporter: ErrorReporter): Parser{
581595
function isSequenceEnd(tk: Token){
582596
return tk.type === TokenType.EOF ||
583597
tk.type === TokenType.EGROUP ||
584-
tk.type === TokenType.OTHER && tk.text === '|' ||
598+
tk.type === TokenType.OTHER && tk.text === ',' ||
585599
isMacro(tk, 'v') ||
586600
isMacro(tk, 'track');
587601
}
@@ -592,19 +606,25 @@ export function createParser(eReporter: ErrorReporter): Parser{
592606
let tk = peek();
593607
let list = new EventNodeList();
594608
while (!isSequenceEnd(tk)){
595-
let slot: INodeSlot = null;
596-
if (tk.type === TokenType.BGROUP){
597-
let group = parseGroup();
598-
slot = list.concat(group);
599-
}
600-
else if (tk.type === TokenType.MACRO){
601-
let node = parseDirective();
602-
node && list.append(node);
609+
if (tk.type === TokenType.OTHER && tk.text === '|'){
610+
// ignore bar lines
611+
next();
603612
}
604613
else {
605-
slot = list.append(parseNote(tk));
614+
let slot: INodeSlot = null;
615+
if (tk.type === TokenType.BGROUP){
616+
let group = parseGroup();
617+
slot = list.concat(group);
618+
}
619+
else if (tk.type === TokenType.MACRO){
620+
let node = parseDirective();
621+
node && list.append(node);
622+
}
623+
else {
624+
slot = list.append(parseNote(tk));
625+
}
626+
parseNoteModifiers(slot);
606627
}
607-
parseNoteModifiers(slot);
608628
tk = peek();
609629
}
610630
return list;
@@ -690,7 +710,7 @@ export function createParser(eReporter: ErrorReporter): Parser{
690710
*/
691711
function parseNoteModifiers(slot: INodeSlot){
692712
let tk = peek();
693-
while (tk.type === TokenType.OTHER && tk.text !== '|' && modifierToType.hasOwnProperty(tk.text)){
713+
while (tk.type === TokenType.OTHER && tk.text !== ',' && modifierToType.hasOwnProperty(tk.text)){
694714
let type = modifierToType[tk.text];
695715
if (modifierWithVal.hasOwnProperty(tk.text)){
696716
let s = tk.text;
@@ -741,7 +761,7 @@ interface NodeWithEvent {
741761
event: MidiEvent;
742762
};
743763

744-
export function createNoteQueue(list: EventNodeList): ISortedNoteEventQueue{
764+
export function createNoteQueue(list: EventNodeList, division: number): ISortedNoteEventQueue{
745765
let queue: List<NodeWithEvent> = new List();
746766
let lastDelta = 0;
747767

@@ -752,7 +772,7 @@ export function createNoteQueue(list: EventNodeList): ISortedNoteEventQueue{
752772
};
753773

754774
function getNoteFromNode(node: NoteEventNode | RestEventNode): Note{
755-
let ret = new Note(node.type === EventNodeType.NOTE ? node.note : Note.REST);
775+
let ret = new Note(node.type === EventNodeType.NOTE ? node.note : Note.REST, division);
756776
for (let n = node.parent; n; n = n.parent){
757777
switch (n.type){
758778
case ModifierNodeType.DASH:

0 commit comments

Comments
 (0)