diff --git a/music21/scale/scala/__init__.py b/music21/scale/scala/__init__.py index f16122454..a937287f2 100644 --- a/music21/scale/scala/__init__.py +++ b/music21/scale/scala/__init__.py @@ -288,10 +288,12 @@ def parse(self) -> None: self.description = line elif count == 2: if line != '': - self.pitchCount = int(line) + # only the first token is data; the format ignores any + # trailing characters, so a comment can follow the value + self.pitchCount = int(line.split()[0]) else: # remaining counts are pitches if line != '': - sp = ScalaPitch(line) + sp = ScalaPitch(line.split()[0]) sp.parse() self.pitchValues.append(sp) @@ -744,6 +746,57 @@ def testScalaFileA(self) -> None: '', '']) + def testCommentAfterAPitchValue(self) -> None: + # noinspection SpellCheckingInspection + msg = r'''! commented.scl +! +A scale whose values carry trailing comments + 3 +! + 10/9 ! A\ ; 8 |mi|la|re| + 454.75 (427) + 2/1 ! octave +''' + ss = ScalaData(msg) + ss.parse() + self.assertEqual(ss.pitchCount, 3) + self.assertEqual(len(ss.pitchValues), 3) + self.assertEqual([f'{x.cents:.5f}' for x in ss.pitchValues], + ['182.40371', '454.75000', '1200.00000']) + + # two bundled files the old parser read without raising, and got + # wrong: the comment's digits were appended to the value + newton = parse('newton_15_out_of_53') + assert newton is not None + self.assertEqual(newton.pitchValues[0].src, '10/9') + self.assertEqual(f'{newton.pitchValues[0].cents:.5f}', '182.40371') + + loops = parse('sparschuh-jsbloops440') + assert loops is not None + self.assertEqual(loops.pitchValues[8].src, '1760/1055') + self.assertEqual(f'{loops.pitchValues[8].cents:.5f}', '885.99892') + + def testEveryBundledScalaFileParses(self) -> None: + # a guard on the archive itself: a file added or edited upstream that + # this parser cannot read should fail here rather than at the point + # some scale is asked for. The description line is not checked, as the + # format allows it to be empty. + paths = getPaths() + self.assertGreater(len(paths), 3000) + for fp in paths: + sf = ScalaFile() + sf.open(fp) + try: + ss = sf.read() + finally: + sf.close() + self.assertIsNotNone(ss.pitchCount, fp) + self.assertEqual(len(ss.pitchValues), ss.pitchCount, fp) + for sp in ss.pitchValues: + self.assertIsNotNone(sp.cents, f'{fp}: {sp.src!r}') + self.assertTrue(math.isfinite(t.cast(float, sp.cents)), + f'{fp}: {sp.src!r}') + # ------------------------------------------------------------------------------ # define presented order in documentation