takomusic 1.3.10 → 1.3.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  // Interpreter for MFS language
2
- import { makeInt, makeFloat, makeString, makeBool, makePitch, makeDur, makeTime, makeArray, makeObject, makeFunction, makeNull, toNumber, isTruthy, toString, deepClone, } from './runtime.js';
2
+ import { makeInt, makeFloat, makeString, makeBool, makePitch, makeDur, makeDurTicks, makeTime, makeArray, makeObject, makeFunction, makeNull, toNumber, isTruthy, toString, deepClone, } from './runtime.js';
3
3
  import { Scope } from './scope.js';
4
4
  import { createError, MFError } from '../errors.js';
5
5
  import * as coreBuiltins from './builtins/core.js';
@@ -463,11 +463,21 @@ export class Interpreter {
463
463
  case 'PitchLiteral':
464
464
  return makePitch(expr.midi);
465
465
  case 'DurLiteral':
466
- // Validate: Dur cannot be negative or zero
466
+ // Handle tick-based duration
467
+ if (expr.ticks !== undefined) {
468
+ if (expr.ticks <= 0) {
469
+ throw new MFError('E101', `Invalid duration: ${expr.ticks}t (must be positive)`, expr.position, this.filePath);
470
+ }
471
+ return makeDurTicks(expr.ticks);
472
+ }
473
+ // Handle fraction/note-based duration
474
+ if (expr.numerator === undefined || expr.denominator === undefined) {
475
+ throw new MFError('E101', 'Invalid duration literal', expr.position, this.filePath);
476
+ }
467
477
  if (expr.numerator <= 0 || expr.denominator <= 0) {
468
478
  throw new MFError('E101', `Invalid duration: ${expr.numerator}/${expr.denominator} (must be positive)`, expr.position, this.filePath);
469
479
  }
470
- return makeDur(expr.numerator, expr.denominator, expr.dots);
480
+ return makeDur(expr.numerator, expr.denominator, expr.dots ?? 0);
471
481
  case 'TimeLiteral':
472
482
  return makeTime(expr.bar, expr.beat, expr.sub);
473
483
  case 'ArrayLiteral': {
@@ -660,6 +670,14 @@ export class Interpreter {
660
670
  }
661
671
  // Dur + Dur
662
672
  if (left.type === 'dur' && right.type === 'dur') {
673
+ // Both must be fraction-based for arithmetic
674
+ if (left.ticks !== undefined || right.ticks !== undefined) {
675
+ throw createError('E400', 'Cannot perform arithmetic on tick-based durations', leftExpr.position, this.filePath);
676
+ }
677
+ if (left.numerator === undefined || left.denominator === undefined ||
678
+ right.numerator === undefined || right.denominator === undefined) {
679
+ throw createError('E400', 'Invalid duration for arithmetic', leftExpr.position, this.filePath);
680
+ }
663
681
  const num = left.numerator * right.denominator + right.numerator * left.denominator;
664
682
  const den = left.denominator * right.denominator;
665
683
  const gcd = this.gcd(num, den);
@@ -689,6 +707,13 @@ export class Interpreter {
689
707
  if (op === '*') {
690
708
  // Dur * Int
691
709
  if (left.type === 'dur' && right.type === 'int') {
710
+ // Handle tick-based duration
711
+ if (left.ticks !== undefined) {
712
+ return makeDurTicks(left.ticks * right.value);
713
+ }
714
+ if (left.numerator === undefined || left.denominator === undefined) {
715
+ throw createError('E400', 'Invalid duration for arithmetic', leftExpr.position, this.filePath);
716
+ }
692
717
  const num = left.numerator * right.value;
693
718
  const gcd = this.gcd(num, left.denominator);
694
719
  return makeDur(num / gcd, left.denominator / gcd);
@@ -707,6 +732,13 @@ export class Interpreter {
707
732
  if (right.value === 0) {
708
733
  throw createError('E400', 'Division by zero', leftExpr.position, this.filePath);
709
734
  }
735
+ // Handle tick-based duration
736
+ if (left.ticks !== undefined) {
737
+ return makeDurTicks(Math.round(left.ticks / right.value));
738
+ }
739
+ if (left.numerator === undefined || left.denominator === undefined) {
740
+ throw createError('E400', 'Invalid duration for arithmetic', leftExpr.position, this.filePath);
741
+ }
710
742
  const den = left.denominator * right.value;
711
743
  const gcd = this.gcd(left.numerator, den);
712
744
  if (gcd === 0) {
@@ -7714,10 +7746,20 @@ export class Interpreter {
7714
7746
  let fromNote = { numerator: 1, denominator: 4 };
7715
7747
  let toNote = { numerator: 1, denominator: 4 };
7716
7748
  if (fromNoteArg.type === 'dur') {
7717
- fromNote = { numerator: fromNoteArg.numerator, denominator: fromNoteArg.denominator };
7749
+ if (fromNoteArg.ticks !== undefined) {
7750
+ throw new MFError('TYPE', 'metricMod() requires fraction-based durations, not tick-based', position, this.filePath);
7751
+ }
7752
+ if (fromNoteArg.numerator !== undefined && fromNoteArg.denominator !== undefined) {
7753
+ fromNote = { numerator: fromNoteArg.numerator, denominator: fromNoteArg.denominator };
7754
+ }
7718
7755
  }
7719
7756
  if (toNoteArg.type === 'dur') {
7720
- toNote = { numerator: toNoteArg.numerator, denominator: toNoteArg.denominator };
7757
+ if (toNoteArg.ticks !== undefined) {
7758
+ throw new MFError('TYPE', 'metricMod() requires fraction-based durations, not tick-based', position, this.filePath);
7759
+ }
7760
+ if (toNoteArg.numerator !== undefined && toNoteArg.denominator !== undefined) {
7761
+ toNote = { numerator: toNoteArg.numerator, denominator: toNoteArg.denominator };
7762
+ }
7721
7763
  }
7722
7764
  if (!track.metricModulations)
7723
7765
  track.metricModulations = [];
@@ -8493,11 +8535,40 @@ export class Interpreter {
8493
8535
  }
8494
8536
  }
8495
8537
  durToTicks(dur, position) {
8538
+ // Handle tick-based duration directly
8539
+ if (dur.ticks !== undefined) {
8540
+ let ticks = dur.ticks;
8541
+ // Apply nested tuplet ratios
8542
+ if (this.currentTrack?.tupletStack && this.currentTrack.tupletStack.length > 0) {
8543
+ for (const tuplet of this.currentTrack.tupletStack) {
8544
+ ticks = (ticks * tuplet.normal) / tuplet.actual;
8545
+ }
8546
+ ticks = Math.round(ticks);
8547
+ }
8548
+ if (ticks < 1) {
8549
+ throw createError('E101', `Duration too small after tuplet application`, position, this.filePath);
8550
+ }
8551
+ return ticks;
8552
+ }
8553
+ // Handle fraction-based duration
8554
+ if (dur.numerator === undefined || dur.denominator === undefined) {
8555
+ throw createError('E101', 'Invalid duration value', position, this.filePath);
8556
+ }
8496
8557
  if (this.ir.ppq === 0) {
8497
8558
  throw createError('E001', 'ppq not set', position, this.filePath);
8498
8559
  }
8499
8560
  // ticks = ppq * 4 * n / d
8500
8561
  let ticks = (this.ir.ppq * 4 * dur.numerator) / dur.denominator;
8562
+ // Apply dotted note multiplier
8563
+ if (dur.dots && dur.dots > 0) {
8564
+ let multiplier = 1;
8565
+ let addition = 0.5;
8566
+ for (let i = 0; i < dur.dots; i++) {
8567
+ multiplier += addition;
8568
+ addition /= 2;
8569
+ }
8570
+ ticks *= multiplier;
8571
+ }
8501
8572
  // Apply nested tuplet ratios (multiply all normal/actual ratios)
8502
8573
  if (this.currentTrack?.tupletStack && this.currentTrack.tupletStack.length > 0) {
8503
8574
  for (const tuplet of this.currentTrack.tupletStack) {