linked-rolls 0.21.0 → 0.23.0

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.
package/lib/Quantity.d.ts CHANGED
@@ -7,8 +7,12 @@
7
7
  * The unit names are the ones the records state in their `unit` field.
8
8
  */
9
9
  declare const unit: unique symbol;
10
+ /**
11
+ * The units a record may state. A `Measure` is limited to these, since
12
+ * they are what the `unit` field is allowed to say.
13
+ */
10
14
  export type Unit = 'mm' | 'cm' | 'px' | 'track' | 's' | 'ms' | 'ft/min' | 'm/min' | 'px/in';
11
- export type Quantity<U extends Unit> = number & {
15
+ export type Quantity<U extends string> = number & {
12
16
  readonly [unit]: U;
13
17
  };
14
18
  /** A place along the roll, or any length on the paper. */
@@ -24,6 +28,8 @@ export type FeetPerMinute = Quantity<'ft/min'>;
24
28
  export type MetersPerMinute = Quantity<'m/min'>;
25
29
  /** How finely a scan was read: pixels of the image per inch of paper. */
26
30
  export type Resolution = Quantity<'px/in'>;
31
+ /** Names a number in a unit. Partially apply it to make a constructor. */
32
+ export declare const quantity: <U extends string>(value: number) => Quantity<U>;
27
33
  export declare const mm: (value: number) => Quantity<"mm">;
28
34
  export declare const cm: (value: number) => Quantity<"cm">;
29
35
  export declare const px: (value: number) => Quantity<"px">;
@@ -50,19 +56,24 @@ export interface Measure<U extends Unit> {
50
56
  */
51
57
  unit: U;
52
58
  }
53
- export declare const add: <U extends Unit>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
54
- export declare const subtract: <U extends Unit>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
55
- export declare const distance: <U extends Unit>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
59
+ export declare const add: <U extends string>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
60
+ export declare const subtract: <U extends string>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
61
+ export declare const distance: <U extends string>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
56
62
  /** A quantity times a plain factor, such as a stretch. */
57
- export declare const scale: <U extends Unit>(a: Quantity<U>, factor: number) => Quantity<U>;
58
- export declare const sum: <U extends Unit>(values: readonly Quantity<U>[]) => Quantity<U>;
59
- export declare const mean: <U extends Unit>(values: readonly Quantity<U>[]) => Quantity<U>;
63
+ export declare const scale: <U extends string>(a: Quantity<U>, factor: number) => Quantity<U>;
64
+ export declare const min: <U extends string>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
65
+ export declare const max: <U extends string>(a: Quantity<U>, b: Quantity<NoInfer<U>>) => Quantity<U>;
66
+ /** The value brought inside the bounds, which must not be the wrong way round. */
67
+ export declare const clamp: <U extends string>(value: Quantity<U>, low: Quantity<NoInfer<U>>, high: Quantity<NoInfer<U>>) => Quantity<U>;
68
+ export declare const sum: <U extends string>(values: readonly Quantity<U>[]) => Quantity<U>;
69
+ export declare const mean: <U extends string>(values: readonly Quantity<U>[]) => Quantity<U>;
60
70
  /** A place in a scan taken at `dpi` dots per inch, on the paper. */
61
71
  export declare const inMillimeters: (place: Pixels, dpi: number) => Millimeters;
62
72
  /** A place on the paper, in a scan taken at `dpi` dots per inch. */
63
73
  export declare const inPixels: (place: Millimeters, dpi: number) => Pixels;
64
74
  export declare const inCentimeters: (length: Millimeters) => Centimeters;
65
75
  export declare const inSeconds: (time: Milliseconds) => Seconds;
76
+ export declare const inMilliseconds: (time: Seconds) => Milliseconds;
66
77
  /** A speed as a record states it, in feet or metres per minute. */
67
78
  export type SpeedMeasure = Measure<'ft/min'> | Measure<'m/min'>;
68
79
  /** A speed in metres per minute, whichever unit it was stated in. */
package/lib/Quantity.js CHANGED
@@ -1,4 +1,5 @@
1
- const quantity = (value) => value;
1
+ /** Names a number in a unit. Partially apply it to make a constructor. */
2
+ export const quantity = (value) => value;
2
3
  export const mm = quantity;
3
4
  export const cm = quantity;
4
5
  export const px = quantity;
@@ -15,6 +16,12 @@ export const subtract = (a, b) => quantity(a - b);
15
16
  export const distance = (a, b) => quantity(Math.abs(a - b));
16
17
  /** A quantity times a plain factor, such as a stretch. */
17
18
  export const scale = (a, factor) => quantity(a * factor);
19
+ // Math.min and Math.max are declared over plain numbers, so a quantity
20
+ // passed through them comes back without its unit.
21
+ export const min = (a, b) => quantity(Math.min(a, b));
22
+ export const max = (a, b) => quantity(Math.max(a, b));
23
+ /** The value brought inside the bounds, which must not be the wrong way round. */
24
+ export const clamp = (value, low, high) => quantity(Math.min(high, Math.max(low, value)));
18
25
  // A list is taken at its element type, so one that mixes units yields a
19
26
  // union unit rather than an error; only the binary operations reject a mix.
20
27
  export const sum = (values) => quantity(values.reduce((total, value) => total + value, 0));
@@ -25,7 +32,9 @@ export const inMillimeters = (place, dpi) => mm(place / dpi * MM_PER_INCH);
25
32
  /** A place on the paper, in a scan taken at `dpi` dots per inch. */
26
33
  export const inPixels = (place, dpi) => px(place / MM_PER_INCH * dpi);
27
34
  export const inCentimeters = (length) => cm(length / 10);
28
- export const inSeconds = (time) => seconds(time / 1000);
35
+ const MS_PER_SECOND = 1000;
36
+ export const inSeconds = (time) => seconds(time / MS_PER_SECOND);
37
+ export const inMilliseconds = (time) => milliseconds(time * MS_PER_SECOND);
29
38
  const METERS_PER_FOOT = 0.3048;
30
39
  /** A speed in metres per minute, whichever unit it was stated in. */
31
40
  export const inMetersPerMinute = (speed) => speed.unit === 'm/min' ? speed.value : metersPerMinute(speed.value * METERS_PER_FOOT);
package/lib/RollCopy.d.ts CHANGED
@@ -332,7 +332,9 @@ export declare const barOf: (copy: Pick<RollCopy, 'production'>) => TrackerBar;
332
332
  export declare function asSymbols(features: AnyFeature[], bar: TrackerBar): AnySymbol[];
333
333
  /**
334
334
  * The tracks a copy carries holes on that the tracker bar does not read.
335
- * A non-empty result usually means the scan is calibrated wrongly.
335
+ * A non-empty result usually means the scan is calibrated wrongly. A hole
336
+ * lying across several positions is counted against each one the bar
337
+ * cannot read, and not at all where it reads them all.
336
338
  */
337
339
  export declare function unreadTracks(features: AnyFeature[], bar: TrackerBar): Map<Track, number>;
338
340
  /**
package/lib/RollCopy.js CHANGED
@@ -40,30 +40,27 @@ export function asSymbols(features, bar) {
40
40
  const end = bar.endsAt(holes);
41
41
  return holes
42
42
  .filter(feature => end === undefined || feature.horizontal.from <= end.at)
43
- .flatMap((feature) => {
44
- const meaning = bar.meaningOf(feature.vertical.from);
45
- if (!meaning)
46
- return [];
47
- return [{
48
- id: `symbol_${v4()}`,
49
- ...meaning,
50
- carriers: [assignReference(feature.id)]
51
- }];
52
- });
43
+ // An opening across two positions uncovers both bar holes and so
44
+ // reads as both commands; one on a single position gives the one.
45
+ .flatMap((feature) => bar.meaningsOf(feature.vertical).map(meaning => ({
46
+ id: `symbol_${v4()}`,
47
+ ...meaning,
48
+ carriers: [assignReference(feature.id)]
49
+ })));
53
50
  }
54
51
  /**
55
52
  * The tracks a copy carries holes on that the tracker bar does not read.
56
- * A non-empty result usually means the scan is calibrated wrongly.
53
+ * A non-empty result usually means the scan is calibrated wrongly. A hole
54
+ * lying across several positions is counted against each one the bar
55
+ * cannot read, and not at all where it reads them all.
57
56
  */
58
57
  export function unreadTracks(features, bar) {
59
58
  const counts = new Map();
60
59
  features
61
60
  .filter(feature => feature.type === 'Hole')
62
- .filter(feature => !bar.meaningOf(feature.vertical.from))
63
- .forEach(feature => {
64
- const position = feature.vertical.from;
65
- counts.set(position, (counts.get(position) || 0) + 1);
66
- });
61
+ .flatMap(feature => bar.positionsIn(feature.vertical))
62
+ .filter(position => !bar.meaningOf(position))
63
+ .forEach(position => counts.set(position, (counts.get(position) || 0) + 1));
67
64
  return counts;
68
65
  }
69
66
  /**
@@ -67,8 +67,20 @@ export interface TrackerBar {
67
67
  * the symbols afterwards.
68
68
  */
69
69
  endsAt(features: readonly PlacedOnBar[]): RollEnd | undefined;
70
- /** `undefined` for a position the bar does not read. */
70
+ /**
71
+ * `undefined` for a position the bar does not read. A measured place
72
+ * is snapped to the nearest position: perforations sit on the grid and
73
+ * measurements of them scatter around it.
74
+ */
71
75
  meaningOf(position: Track): TrackMeaning | undefined;
76
+ /**
77
+ * What the bar reads off a feature, which may lie across more than one
78
+ * position. A perforation lifts every valve whose bar hole it uncovers,
79
+ * so an opening across two positions reads as two commands; one on a
80
+ * single position reads as the one `meaningOf` gives, and one on
81
+ * positions the bar does not read as none at all.
82
+ */
83
+ meaningsOf(span: OnBar): readonly TrackMeaning[];
72
84
  /**
73
85
  * `meaningOf` inverted: the position this bar reads the meaning on,
74
86
  * or `undefined` where it does not read it at all. A symbol's track
@@ -76,8 +88,10 @@ export interface TrackerBar {
76
88
  * sits on exactly one position of a given bar.
77
89
  */
78
90
  positionOf(meaning: TrackMeaning): Track | undefined;
79
- /** `undefined` for a position the bar does not read. */
91
+ /** `undefined` for a position the bar does not read; snapped as `meaningOf` is. */
80
92
  roleOf(position: Track): TrackRole | undefined;
93
+ /** The positions a feature lies across, snapped to the grid. */
94
+ positionsIn(span: OnBar): readonly Track[];
81
95
  }
82
96
  /**
83
97
  * What a position says, as a key. Two bars read the same thing exactly
@@ -118,6 +132,15 @@ export type PlacedOnBar = {
118
132
  readonly from: Track;
119
133
  };
120
134
  };
135
+ /**
136
+ * Where a feature lies across the bar: one place, or a run of them where
137
+ * `to` is given. Structural, so a feature's `vertical` passes as it is and
138
+ * this module need know nothing about features.
139
+ */
140
+ export type OnBar = {
141
+ readonly from: Track;
142
+ readonly to?: Track;
143
+ };
121
144
  export interface TrackerBarSpec {
122
145
  id: string;
123
146
  name: string;
package/lib/TrackerBar.js CHANGED
@@ -20,24 +20,42 @@ const areasOf = ({ notes, trackCount }) => [
20
20
  { role: 'treble-expression', from: track(notes.to + 1), to: track(trackCount) }
21
21
  ];
22
22
  const scopeOf = (role) => role === 'bass-expression' ? 'bass' : 'treble';
23
+ /**
24
+ * The bar position a measured place falls on. Places are measured off a
25
+ * scan and scatter around the grid, while the bar has holes only at whole
26
+ * positions, so the nearest one is the one uncovered.
27
+ */
28
+ const snap = (place) => track(Math.round(place));
29
+ /** The positions a span reaches, both ends snapped and the run between them. */
30
+ const positionsBetween = (span) => {
31
+ const ends = [snap(span.from), snap(span.to ?? span.from)];
32
+ const [first, last] = [Math.min(...ends), Math.max(...ends)];
33
+ return Array.from({ length: last - first + 1 }, (_, step) => track(first + step));
34
+ };
23
35
  export const describeTrackerBar = (spec) => {
24
36
  const areas = areasOf(spec);
25
- const roleOf = (position) => areas.find(area => position >= area.from && position <= area.to)?.role;
37
+ const areaAt = (position) => areas.find(area => position >= area.from && position <= area.to)?.role;
38
+ const roleOf = (position) => areaAt(snap(position));
26
39
  const meaningOf = (position) => {
27
- const role = roleOf(position);
40
+ const place = snap(position);
41
+ const role = areaAt(place);
28
42
  if (!role)
29
43
  return undefined;
30
44
  if (role === 'note') {
31
45
  return {
32
46
  type: 'note',
33
- pitch: position - spec.notes.from + spec.notes.lowestPitch
47
+ pitch: place - spec.notes.from + spec.notes.lowestPitch
34
48
  };
35
49
  }
36
- const expressionType = spec.expressions.get(position);
50
+ const expressionType = spec.expressions.get(place);
37
51
  if (!expressionType)
38
52
  return undefined;
39
53
  return { type: 'expression', expressionType, scope: scopeOf(role) };
40
54
  };
55
+ const meaningsOf = (span) => positionsBetween(span).flatMap(position => {
56
+ const meaning = meaningOf(position);
57
+ return meaning ? [meaning] : [];
58
+ });
41
59
  const positions = new Map(Array.from({ length: spec.trackCount }, (_, i) => track(i + 1))
42
60
  .flatMap(position => {
43
61
  const meaning = meaningOf(position);
@@ -54,7 +72,7 @@ export const describeTrackerBar = (spec) => {
54
72
  const endsAt = (features) => {
55
73
  const hold = spec.rewindHold ?? mm(0);
56
74
  const candidates = features
57
- .filter(feature => feature.vertical.from === rewind)
75
+ .filter(feature => snap(feature.vertical.from) === rewind)
58
76
  .filter(feature => !shared || feature.horizontal.to - feature.horizontal.from >= hold)
59
77
  .map(feature => feature.horizontal.from);
60
78
  return candidates.length ? { at: mm(Math.min(...candidates)), because: 'rewind' } : undefined;
@@ -70,8 +88,10 @@ export const describeTrackerBar = (spec) => {
70
88
  rewindTrack: track(rewind),
71
89
  ...(spec.paperSpeed && { paperSpeed: spec.paperSpeed }),
72
90
  meaningOf,
91
+ meaningsOf,
73
92
  positionOf: meaning => positions.get(keyOf(meaning)),
74
- roleOf
93
+ roleOf,
94
+ positionsIn: positionsBetween
75
95
  };
76
96
  };
77
97
  /**
package/lib/alignment.js CHANGED
@@ -59,8 +59,9 @@ const noteOnsets = (features, bar) => features
59
59
  .flatMap((feature) => {
60
60
  if (feature.type !== 'Hole')
61
61
  return [];
62
- const meaning = bar.meaningOf(feature.vertical.from);
63
- return meaning?.type === 'note' ? [{ pitch: meaning.pitch, at: feature.horizontal.from }] : [];
62
+ return bar.meaningsOf(feature.vertical)
63
+ .filter(meaning => meaning.type === 'note')
64
+ .map(meaning => ({ pitch: meaning.pitch, at: feature.horizontal.from }));
64
65
  })
65
66
  .sort(byPlace);
66
67
  const median = (values) => {
@@ -79,8 +79,10 @@ const carriersOffTheirMeaning = (view, version, perforations) => {
79
79
  const copy = view.copyOf(carrier.id);
80
80
  if (!copy)
81
81
  return false;
82
- const meaning = barOf(copy).meaningOf(carrier.vertical.from);
83
- return !meaning || keyOf(meaning) !== keyOf(symbol);
82
+ // A carrier lying across several positions reads as several commands,
83
+ // and carries the symbol as long as one of them is the symbol's.
84
+ return !barOf(copy).meaningsOf(carrier.vertical)
85
+ .some(meaning => keyOf(meaning) === keyOf(symbol));
84
86
  };
85
87
  return perforations
86
88
  .filter(symbol => view.carriersOf(symbol).some(carrier => misread(carrier, symbol)))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system",
5
5
  "license": "MIT",
6
6
  "repository": {