diff --git a/docs/pentlyas.md b/docs/pentlyas.md index 8b0df05..2dc1520 100644 --- a/docs/pentlyas.md +++ b/docs/pentlyas.md @@ -446,7 +446,16 @@ slurred group. This is useful for tying notes together or producing legato (HOPO). Slurring into a note with the same pitch is the same as a wait: `eb2~ eb8`, `eb2( eb8)`, and `eb2 w8` mean the same. -Notes are separated by at least one space. +Notes are separated by at least one space. Notes are added to a +pattern until it is closed by one of these commands: `title`, +`author`, `copyright`, `drum`, `song`, `segno`, `fine`, `dal segno`, +`da capo`, `at`, `attack`, `play`, `stop`, or `fallthrough`. + +If a pattern ends with the `fallthrough` command, playback continues +into the following pattern in the score. For example, if pattern +`AAA` falls through and the next pattern is `BBB`, then playing `AAA` +produces `AAA`, `BBB`, `AAA`, `BBB`, ..., while playing `BBB` +produces `BBB`, `BBB`, ... A simple pattern might look like this: @@ -569,7 +578,8 @@ where `0` means the measure preceding the first full measure, and The `pickup` command also works in patterns for bar check use. If defining a pattern within a song, make sure to specify `pickup` -outside a pattern, such as before the start of the pattern. +outside a pattern definition, such as before the start of the first +pattern or after a command that closes a pattern. In addition to the tracks for pitched channels (`pulse1`, `pulse2`, and `triangle`) and the drum track, Pently has an **attack track** diff --git a/tools/pentlyas.py b/tools/pentlyas.py index 179720f..45fd674 100755 --- a/tools/pentlyas.py +++ b/tools/pentlyas.py @@ -510,6 +510,7 @@ def set_measure(self, measure=1, beat=1, row=0): """Set the current musical time.""" measure, row, _, _ = self.parse_measure(measure, beat, row) self.cur_measure, self.row_in_measure = measure, row + print("set_measure to %d:%d in %x" % (self.cur_measure, self.row_in_measure, id(self))) def add_rows(self, rows): """Add a duration in rows to the current musical time.""" @@ -522,6 +523,7 @@ def wait_for_measure(self, measure, beat=1, row=0): """Seek to a given musical time. Return rows between old and new positions.""" + print("wait from %d:%d in %x" % (self.cur_measure, self.row_in_measure, id(self))) measure, row, measure_length, beat_length = self.parse_measure(measure, beat, row) if (measure < self.cur_measure or (measure == self.cur_measure and row < self.row_in_measure)): @@ -1326,13 +1328,6 @@ def find_transpose_runs(data): runs[-1][1:3] = lo, hi return [tuple(i) for i in runs] - def calc_transpose(self): - if self.track == 'drum': - self.transpose_runs, self.transpose = [], None - return - self.transpose_runs = self.find_transpose_runs(self.notes) - self.transpose = self.transpose_runs[0][1] - @staticmethod def collapse_ties(notes, tie_rests=False): """Interpret slur commands; combine w notes and slurred same-pitch notes. @@ -1423,6 +1418,17 @@ def collapse_effects(notes): return rnotes + def make_final(self): + """Collapse ties, collapse arpeggio effects, and calculate transpose runs""" + pitched = self.track != 'drum' + if self.track == 'drum': + self.transpose_runs, self.transpose = [], None + return + self.notes = self.collapse_ties(self.notes, not pitched) + self.notes = self.collapse_effects(self.notes) + self.transpose_runs = self.find_transpose_runs(self.notes) + self.transpose = self.transpose_runs[0][1] + row_to_duration = [ (16, '|D_1'), (12, '|D_D2'), (8, '|D_2'), (6, '|D_D4'), (4, '|D_4'), (3, '|D_D8'), (2, '|D_8'), (1, '') @@ -1440,11 +1446,8 @@ def numrows_to_durations(self, numrows): def render(self, scopes): is_drum = self.track == 'drum' - notes = self.collapse_ties(self.notes, is_drum) - self.notes = notes = self.collapse_effects(notes) bytedata = [] - if self.transpose is None: self.calc_transpose() transpose_runs, cur_transpose = self.transpose_runs, self.transpose last_slur = False @@ -1452,11 +1455,11 @@ def render(self, scopes): # FALLTHROUGH, schedule a TRANSPOSE for the first note transpose_pos = 1 if (transpose_runs - and (transpose_pos > transpose_runs[0][1] - or transpose_pos - 24 < transpose_runs[0][2])): + and (cur_transpose > transpose_runs[0][1] + or cur_transpose + 24 < transpose_runs[0][2])): transpose_pos = 0 - for i, note in enumerate(notes): + for i, note in enumerate(self.notes): if (transpose_runs and transpose_pos < len(transpose_runs) and i >= transpose_runs[transpose_pos][0]): @@ -2238,13 +2241,16 @@ def render_file(parser, segment='RODATA'): # Propagate transposition base backward revpatterns = sorted(parser.patterns.values(), key=lambda x: x.orderkey, reverse=True) - last_transpose = last_pitched = last_patname = None + last_transpose = last_pitched = last_patname = last_lowest_note = None for pat in revpatterns: - pat.calc_transpose() pitched = pat.track != 'drum' + pat.make_final() + lowest_note = ( + min(x[1] for x in pat.transpose_runs) if pitched else None + ) if not pat.fallthrough: last_patname, last_pitched = pat.name, pitched - last_transpose = pat.transpose + last_transpose, last_lowest_note = pat.transpose, lowest_note if last_patname is None: raise ValueError("%s falls through but is the last pattern" % pat.name) @@ -2254,7 +2260,9 @@ def render_file(parser, segment='RODATA'): % (pat.name, "pitched" if pitched else "drum", last_patname, "pitched" if last_pitched else "drum") ) - pat.transpose = last_transpose + if lowest_note is not None: + last_lowest_note = min(lowest_note, last_lowest_note) + pat.transpose, pat.lowest_note = last_transpose, last_lowest_note # Pack byte arrays that are subsequences of another byte array # into the longer one