xi2

a plain text language that compiles to MIDI
git clone https://git.ce9e.org/xi2.git

commit
89439b0bd26b64261456a9a71b76087845b486ba
parent
365fdeedb85413c1eb0d28a0a20160dd3feb55d1
Author
Tobias Bengfort <tobias.bengfort@posteo.de>
Date
2023-09-19 09:38
rewrite renderer

Diffstat

M renderer.py 119 +++++++++++++++++++++++--------------------------------------
M xi2.py 4 ++--

2 files changed, 47 insertions, 76 deletions


diff --git a/renderer.py b/renderer.py

@@ -1,100 +1,71 @@
    1     1 import midi
    2     2 
    3    -1 # The tricky part here is the time conversion.
    4    -1 # and noteOff events
    5    -1 
    6     3 
    7     4 class Renderer:
    8     5     """Convert intermediate code to MIDI bytecode."""
    9     6 
   10    -1     def __init__(self, midi, seq, ch=0, offset=60):
   -1     7     def __init__(self, midi, ch=0, offset=60):
   11     8         self.midi = midi
   12     9         self.ch = ch
   13    10         self.offset = offset
   14    11         self.dt = 0
   15    -1         self.dt_stack = []
   16    -1         self.stack = []
   17    -1         self.render_seq(seq)
   18    -1 
   19    -1     def dt_step(self):
   20    -1         a = 1.0
   21    -1         for i in self.dt_stack[1:]:
   22    -1             a /= i
   23    -1         return a
   -1    12         self.active_notes = set()
   24    13 
   25    -1     def render_element(self, e):
   26    -1         if e.isdigit():  # note
   27    -1             self.midi.note_on(self.dt, self.ch, self.offset + int(e), 1)
   28    -1             self.dt = self.dt_step()
   29    -1         elif e == '-':  # hold
   30    -1             self.dt += self.dt_step()
   31    -1         elif e == '':  # break
   32    -1             self.dt += self.dt_step()
   33    -1         else:  # lyrics
   34    -1             self.midi.lyrics(self.dt, e.replace('_', ' '))
   35    -1             self.dt = self.dt_step()
   -1    14     def note_on(self, s):
   -1    15         note = int(s, 10) + self.offset
   -1    16         self.midi.note_on(self.dt, self.ch, note, 1)
   -1    17         self.active_notes.add(note)
   -1    18         self.dt = 0
   36    19 
   37    -1     def render_seq(self, seq):
   38    -1         self.dt_stack.append(len(seq))
   39    -1         for e in seq:
   40    -1             if isinstance(e, str):
   41    -1                 if e != '-':
   42    -1                     self.stop()
   43    -1                 self.stack.append(e)
   44    -1                 self.render_element(e)
   45    -1             elif isinstance(e, set):
   46    -1                 if '-' not in e:
   47    -1                     self.stop()
   48    -1                 self.stack.append(e)
   49    -1                 self.render_chord(e)
   50    -1             elif isinstance(e, list):
   51    -1                 self.render_seq(e)
   52    -1             else:
   53    -1                 raise ValueError("unknown element: " + e)
   54    -1         self.dt_stack.pop()
   -1    20     def lyrics(self, s):
   -1    21         self.midi.lyrics(self.dt, s.replace('_', ' '))
   -1    22         self.dt = 0
   55    23 
   56    -1     def render_chord(self, s):
   57    -1         for e in s:
   58    -1             if not isinstance(e, str):
   59    -1                 raise ValueError("only elements are allowed inside chords: " + e)
   60    -1             elif e == '':
   61    -1                 raise ValueError("Breaks are not allowed inside sets!")
   62    -1             else:
   63    -1                 self.render_element(e)
   -1    24     def stop(self):
   -1    25         while self.active_notes:
   -1    26             self.midi.note_off(self.dt, self.ch, self.active_notes.pop(), 1)
   64    27             self.dt = 0
   65    -1         self.dt = self.dt_step()
   66    28 
   67    -1     def stop(self):
   68    -1         if len(self.stack) == 0:
   69    -1             return
   70    -1         e = self.stack.pop()
   71    -1         if isinstance(e, str):
   72    -1             if e == '-':
   -1    29     def render_seq(self, seq, step=1.0):
   -1    30         for item in seq:
   -1    31             if isinstance(item, set):
   -1    32                 if '-' not in item:
   -1    33                     self.stop()
   -1    34                 for subitem in item:
   -1    35                     if subitem == '-':
   -1    36                         continue
   -1    37                     elif subitem.isdigit():
   -1    38                         self.note_on(subitem)
   -1    39                     else:
   -1    40                         self.lyrics(subitem)
   -1    41                 self.dt = step
   -1    42             elif isinstance(item, list):
   -1    43                 self.render_seq(item, step / len(item))
   -1    44             elif item == '-':
   -1    45                 self.dt += step
   -1    46             elif item == '':
   73    47                 self.stop()
   74    -1             elif e == '':
   75    -1                 pass  # already stopped
   76    -1             elif e.isdigit():
   77    -1                 self.midi.note_off(self.dt, self.ch, self.offset + int(e), 1)
   78    -1                 self.dt = 0
   -1    48                 self.dt += step
   -1    49             elif item.isdigit():
   -1    50                 self.stop()
   -1    51                 self.note_on(item)
   -1    52                 self.dt = step
   79    53             else:
   80    -1                 pass
   81    -1         elif isinstance(e, set):
   82    -1             if '-' in e:
   83    54                 self.stop()
   84    -1             for ee in e:
   85    -1                 # we already checked the validity of the set when parsing.
   86    -1                 # we only need to check if this is a note, lyrics or '-'
   87    -1                 if ee.isdigit():
   88    -1                     self.midi.note_off(self.dt, self.ch, self.offset + int(ee), 1)
   89    -1                     self.dt = 0
   90    -1         else:
   91    -1             assert False, "Unexpected object on stack: " + e
   -1    55                 self.lyrics(item)
   -1    56                 self.dt = step
   -1    57 
   -1    58 
   -1    59 def render(seq, midi, ch=0, offset=60):
   -1    60     r = Renderer(midi, ch, offset)
   -1    61     r.render_seq(seq)
   -1    62     r.stop()
   92    63 
   93    64 
   94    65 if __name__ == '__main__':
   95    66     a = [[['0', '1'], '2'], '4', '5', '-', '', {'0', '4', '7'}, '', '', '0', {'3', '-'}]
   96    67     t = midi.Midi()
   97    -1     ip = Renderer(t, a, 0, 60)
   -1    68     render(a, t)
   98    69 
   99    70     with open('test.mid', 'wb') as fh:
  100    71         midi.write_file(fh, [t])

diff --git a/xi2.py b/xi2.py

@@ -4,7 +4,7 @@ import argparse
    4     4 import re
    5     5 
    6     6 import midi
    7    -1 from renderer import Renderer
   -1     7 from renderer import render
    8     8 
    9     9 
   10    10 def parse(t):
@@ -115,7 +115,7 @@ if __name__ == '__main__':
  115   115             prog = 0
  116   116         m.prog_ch(0, ch, prog)
  117   117         # write data
  118    -1         Renderer(m, track, ch=ch, offset=args.offset)
   -1   118         render(track, m, ch=ch, offset=args.offset)
  119   119         # write
  120   120         midi_tracks.append(m)
  121   121         ch += 1