linked-rolls 0.7.0 → 0.9.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.
@@ -1,14 +1,15 @@
1
+ import { px, subtract, track } from "./Quantity";
1
2
  /** Image column at the centre of a tracker bar track. */
2
- export const columnOf = (track, calibration) => calibration.offset + (track - calibration.shift) * calibration.separation;
3
+ export const columnOf = (position, calibration) => px(calibration.offset + (position - calibration.shift) * calibration.separation);
3
4
  /** Tracker bar track covering an image column, unrounded. */
4
- export const trackAt = (column, calibration) => (column - calibration.offset) / calibration.separation + calibration.shift;
5
+ export const trackAt = (column, calibration) => track((column - calibration.offset) / calibration.separation + calibration.shift);
5
6
  /**
6
7
  * The columns covered by a run of tracks, from the outer edge of the
7
8
  * first to the outer edge of the last.
8
9
  */
9
10
  export const columnsOf = (from, to, calibration) => {
10
11
  const [lower, upper] = from <= to ? [from, to] : [to, from];
11
- const start = columnOf(lower, calibration) - calibration.separation / 2;
12
- const end = columnOf(upper, calibration) + calibration.separation / 2;
13
- return { from: start, to: end, width: end - start };
12
+ const start = px(columnOf(lower, calibration) - calibration.separation / 2);
13
+ const end = px(columnOf(upper, calibration) + calibration.separation / 2);
14
+ return { from: start, to: end, width: subtract(end, start) };
14
15
  };
@@ -1,5 +1,6 @@
1
1
  import type { Concept } from "./Agent";
2
2
  import { Expression, Note } from "./Symbol";
3
+ import { Millimeters, Track } from "./Quantity";
3
4
  /**
4
5
  * What a tracker bar position does: sound a note, or operate one of
5
6
  * the expression valves on the bass or the treble side.
@@ -11,8 +12,8 @@ export type TrackRole = 'bass-expression' | 'note' | 'treble-expression';
11
12
  */
12
13
  export interface TrackArea {
13
14
  readonly role: TrackRole;
14
- readonly from: number;
15
- readonly to: number;
15
+ readonly from: Track;
16
+ readonly to: Track;
16
17
  }
17
18
  export type NoteMeaning = Pick<Note, 'type' | 'pitch'>;
18
19
  export type ExpressionMeaning = Pick<Expression, 'type' | 'expressionType' | 'scope'>;
@@ -35,8 +36,8 @@ export interface TrackerBar {
35
36
  */
36
37
  readonly id: string;
37
38
  readonly name: string;
38
- /** Width of the roll the bar reads, in mm. */
39
- readonly width: number;
39
+ /** Width of the roll the bar reads. */
40
+ readonly width: Millimeters;
40
41
  /** Number of positions on the bar. Tracks run from 1 to this. */
41
42
  readonly trackCount: number;
42
43
  /** The blocks of positions, from the bass edge upwards. */
@@ -48,20 +49,25 @@ export interface TrackerBar {
48
49
  * the very end of a roll and is the usual landmark for calibrating
49
50
  * a scan against the bar.
50
51
  */
51
- readonly rewindTrack: number;
52
+ readonly rewindTrack: Track;
52
53
  /** `undefined` for a position the bar does not read. */
53
- meaningOf(track: number): TrackMeaning | undefined;
54
+ meaningOf(position: Track): TrackMeaning | undefined;
54
55
  /** `undefined` for a position the bar does not read. */
55
- roleOf(track: number): TrackRole | undefined;
56
+ roleOf(position: Track): TrackRole | undefined;
56
57
  }
57
58
  /** The roll system a tracker bar belongs to, as the roll metadata states it. */
58
59
  export declare const systemOf: (bar: TrackerBar) => Concept;
59
60
  /** The identifier of a system the type vocabulary knows, from its concept. */
60
61
  export declare const systemIdOf: (system: Concept | undefined) => string | undefined;
62
+ /**
63
+ * A tracker bar as written down, with its positions as plain numbers
64
+ * in the bar's own 1-based numbering; `describeTrackerBar` gives them
65
+ * their type.
66
+ */
61
67
  export interface TrackerBarSpec {
62
68
  id: string;
63
69
  name: string;
64
- width: number;
70
+ width: Millimeters;
65
71
  trackCount: number;
66
72
  /** The contiguous block of note positions. */
67
73
  notes: {
package/lib/TrackerBar.js CHANGED
@@ -1,35 +1,36 @@
1
+ import { track } from "./Quantity";
1
2
  const SYSTEM_IRI = 'https://w3id.org/reo/type/system/';
2
3
  /** The roll system a tracker bar belongs to, as the roll metadata states it. */
3
4
  export const systemOf = (bar) => ({ id: SYSTEM_IRI + bar.id, name: bar.name, sameAs: [] });
4
5
  /** The identifier of a system the type vocabulary knows, from its concept. */
5
6
  export const systemIdOf = (system) => system?.id?.startsWith(SYSTEM_IRI) ? system.id.slice(SYSTEM_IRI.length) : undefined;
6
7
  const areasOf = ({ notes, trackCount }) => [
7
- { role: 'bass-expression', from: 1, to: notes.from - 1 },
8
- { role: 'note', from: notes.from, to: notes.to },
9
- { role: 'treble-expression', from: notes.to + 1, to: trackCount }
8
+ { role: 'bass-expression', from: track(1), to: track(notes.from - 1) },
9
+ { role: 'note', from: track(notes.from), to: track(notes.to) },
10
+ { role: 'treble-expression', from: track(notes.to + 1), to: track(trackCount) }
10
11
  ];
11
12
  const scopeOf = (role) => role === 'bass-expression' ? 'bass' : 'treble';
12
13
  export const describeTrackerBar = (spec) => {
13
14
  const areas = areasOf(spec);
14
- const roleOf = (track) => areas.find(area => track >= area.from && track <= area.to)?.role;
15
- const meaningOf = (track) => {
16
- const role = roleOf(track);
15
+ const roleOf = (position) => areas.find(area => position >= area.from && position <= area.to)?.role;
16
+ const meaningOf = (position) => {
17
+ const role = roleOf(position);
17
18
  if (!role)
18
19
  return undefined;
19
20
  if (role === 'note') {
20
21
  return {
21
22
  type: 'note',
22
- pitch: track - spec.notes.from + spec.notes.lowestPitch
23
+ pitch: position - spec.notes.from + spec.notes.lowestPitch
23
24
  };
24
25
  }
25
- const expressionType = spec.expressions.get(track);
26
+ const expressionType = spec.expressions.get(position);
26
27
  if (!expressionType)
27
28
  return undefined;
28
29
  return { type: 'expression', expressionType, scope: scopeOf(role) };
29
30
  };
30
- const rewindTrack = [...spec.expressions]
31
+ const rewind = [...spec.expressions]
31
32
  .find(([, type]) => type === 'Rewind')?.[0];
32
- if (rewindTrack === undefined) {
33
+ if (rewind === undefined) {
33
34
  throw new Error(`${spec.name} declares no rewind track`);
34
35
  }
35
36
  return {
@@ -39,7 +40,7 @@ export const describeTrackerBar = (spec) => {
39
40
  trackCount: spec.trackCount,
40
41
  areas,
41
42
  expressionTypes: [...new Set(spec.expressions.values())],
42
- rewindTrack,
43
+ rewindTrack: track(rewind),
43
44
  meaningOf,
44
45
  roleOf
45
46
  };
@@ -2,6 +2,7 @@ import { ObjectAssumption } from "./Assumption";
2
2
  import { AnyFeature } from "./Feature";
3
3
  import { PaperStretch, RollCopy, Shift } from "./RollCopy";
4
4
  import { TrackerBar } from "./TrackerBar";
5
+ import { Millimeters } from "./Quantity";
5
6
  export declare const applyShift: (shift: Shift, copy: RollCopy) => void;
6
7
  export declare const applyStretch: (paperStretch: ObjectAssumption<PaperStretch>, copy: RollCopy) => void;
7
8
  /** Takes the shift off the copy's features again, as far as one was applied. */
@@ -9,8 +10,8 @@ export declare const revertShift: (copy: RollCopy) => void;
9
10
  /** Takes the stretch off the copy's features again, as far as one was applied. */
10
11
  export declare const revertStretch: (copy: RollCopy) => void;
11
12
  type AlignmentResult = {
12
- /** Shift in mm, applied before the stretch. */
13
- shift: number;
13
+ /** Applied before the stretch. */
14
+ shift: Millimeters;
14
15
  stretch: number;
15
16
  };
16
17
  /**
package/lib/alignment.js CHANGED
@@ -1,16 +1,24 @@
1
1
  import { welteT100 } from "./systems/welteT100/bar";
2
+ import { add, mm, scale } from "./Quantity";
3
+ /** Moves both ends of a span, the far end only where the span has one. */
4
+ const move = (span, by) => {
5
+ span.from = add(span.from, by);
6
+ if (span.to !== undefined)
7
+ span.to = add(span.to, by);
8
+ };
9
+ /** Stretches both ends of a span away from the beginning of the roll. */
10
+ const stretch = (span, factor) => {
11
+ span.from = scale(span.from, factor);
12
+ if (span.to !== undefined)
13
+ span.to = scale(span.to, factor);
14
+ };
15
+ const back = (shift) => ({ horizontal: scale(shift.horizontal, -1), vertical: scale(shift.vertical, -1) });
2
16
  export const applyShift = (shift, copy) => {
3
17
  if (copy.ops.includes('shifted'))
4
18
  return;
5
19
  copy.features.forEach(feature => {
6
- feature.horizontal.from += shift.horizontal;
7
- if (feature.horizontal.to) {
8
- feature.horizontal.to += shift.horizontal;
9
- }
10
- feature.vertical.from += shift.vertical;
11
- if (feature.vertical.to) {
12
- feature.vertical.to += shift.vertical;
13
- }
20
+ move(feature.horizontal, shift.horizontal);
21
+ move(feature.vertical, shift.vertical);
14
22
  });
15
23
  copy.ops = [...copy.ops, 'shifted'];
16
24
  copy.measurements.shift = shift;
@@ -18,13 +26,7 @@ export const applyShift = (shift, copy) => {
18
26
  export const applyStretch = (paperStretch, copy) => {
19
27
  if (copy.ops.includes('stretched'))
20
28
  return;
21
- const stretch = paperStretch.factor;
22
- copy.features.forEach(feature => {
23
- feature.horizontal.from *= stretch;
24
- if (feature.horizontal.to) {
25
- feature.horizontal.to *= stretch;
26
- }
27
- });
29
+ copy.features.forEach(feature => stretch(feature.horizontal, paperStretch.factor));
28
30
  copy.ops = [...copy.ops, 'stretched'];
29
31
  copy.conditions.push(paperStretch);
30
32
  };
@@ -33,15 +35,10 @@ export const revertShift = (copy) => {
33
35
  const shift = copy.measurements.shift;
34
36
  if (!copy.ops.includes('shifted') || !shift)
35
37
  return;
38
+ const reversed = back(shift);
36
39
  copy.features.forEach(feature => {
37
- feature.horizontal.from -= shift.horizontal;
38
- if (feature.horizontal.to) {
39
- feature.horizontal.to -= shift.horizontal;
40
- }
41
- feature.vertical.from -= shift.vertical;
42
- if (feature.vertical.to) {
43
- feature.vertical.to -= shift.vertical;
44
- }
40
+ move(feature.horizontal, reversed.horizontal);
41
+ move(feature.vertical, reversed.vertical);
45
42
  });
46
43
  copy.ops = copy.ops.filter(op => op !== 'shifted');
47
44
  delete copy.measurements.shift;
@@ -49,15 +46,10 @@ export const revertShift = (copy) => {
49
46
  const isPaperStretch = (condition) => condition.conditionType === 'paper-stretch';
50
47
  /** Takes the stretch off the copy's features again, as far as one was applied. */
51
48
  export const revertStretch = (copy) => {
52
- const stretch = copy.conditions.find(isPaperStretch);
53
- if (!copy.ops.includes('stretched') || !stretch)
49
+ const applied = copy.conditions.find(isPaperStretch);
50
+ if (!copy.ops.includes('stretched') || !applied)
54
51
  return;
55
- copy.features.forEach(feature => {
56
- feature.horizontal.from /= stretch.factor;
57
- if (feature.horizontal.to) {
58
- feature.horizontal.to /= stretch.factor;
59
- }
60
- });
52
+ copy.features.forEach(feature => stretch(feature.horizontal, 1 / applied.factor));
61
53
  copy.ops = copy.ops.filter(op => op !== 'stretched');
62
54
  copy.conditions = copy.conditions.filter(condition => !isPaperStretch(condition));
63
55
  };
@@ -112,6 +104,6 @@ export function alignFeatures(rollA, rollB, bar = welteT100) {
112
104
  const { alpha: alphaB, beta: betaB } = fitIndexToPosition(idxB, XB);
113
105
  // 5. Derive stretch and shift such that x2 = (x1 + shift) * stretch
114
106
  const stretch = alphaB / alphaA;
115
- const shift = betaB / stretch - betaA;
107
+ const shift = mm(betaB / stretch - betaA);
116
108
  return { stretch, shift };
117
109
  }
package/lib/editionOps.js CHANGED
@@ -5,6 +5,7 @@ import { collationsOf, defaultCollationTolerance } from "./Collation";
5
5
  import { asSymbols } from "./RollCopy";
6
6
  import { applyShift, applyStretch, revertShift, revertStretch } from "./alignment";
7
7
  import { assignReference, idOf } from "./Assumption";
8
+ import { distance, mm, subtract } from "./Quantity";
8
9
  const noChange = () => undefined;
9
10
  const onCopy = (copyId, op) => draft => {
10
11
  const copy = draft.copies.find(c => c.id === copyId);
@@ -202,12 +203,14 @@ export const deriveVersion = (versionId, editIds) => onVersion(versionId, (versi
202
203
  const sameSequence = (a, b) => a.length === b.length && a.every((value, i) => value === b[i]);
203
204
  const expressionTypesOf = (symbols) => symbols.filter((symbol) => symbol.type === 'expression').map(symbol => symbol.expressionType);
204
205
  const accents = [['SlowCrescendoOn', 'SlowCrescendoOff'], ['ForzandoOn', 'ForzandoOff']];
205
- const lengthOf = (span) => span.to - span.from;
206
+ const lengthOf = (span) => subtract(span.to, span.from);
207
+ /** How far apart two onsets may lie for the one symbol to count as a replacement of the other. */
208
+ const REPLACEMENT_TOLERANCE = mm(5);
206
209
  /** Shorten or prolong, where the inserted symbol starts about where the deleted one did. */
207
210
  const replacementType = (view, inserted, deleted) => {
208
211
  const after = view.dimensionOf(inserted)?.horizontal;
209
212
  const before = view.dimensionOf(deleted)?.horizontal;
210
- if (!after || !before || Math.abs(after.from - before.from) >= 5)
213
+ if (!after || !before || distance(after.from, before.from) >= REPLACEMENT_TOLERANCE)
211
214
  return undefined;
212
215
  return lengthOf(after) < lengthOf(before) ? 'shorten' : 'prolong';
213
216
  };
package/lib/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './utils';
2
+ export * from './Quantity';
2
3
  export * from './Agent';
3
4
  export * from './Assumption';
4
5
  export * from './ConditionState';
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './utils';
2
+ export * from './Quantity';
2
3
  export * from './Agent';
3
4
  export * from './Assumption';
4
5
  export * from './ConditionState';
package/lib/migrate.js CHANGED
@@ -75,4 +75,8 @@ const withRollSystem = (edition) => {
75
75
  const system = { '@id': id, ...concept, ...(stated && { name: stated }) };
76
76
  return { ...edition, roll: { ...edition.roll, system } };
77
77
  };
78
- export const migrate = (edition) => walk(withRollSystem(edition));
78
+ /** An edition written before the editors were carried names none. */
79
+ const withEditors = (edition) => !edition.creation || edition.creation.editors
80
+ ? edition
81
+ : { ...edition, creation: { ...edition.creation, editors: [] } };
82
+ export const migrate = (edition) => walk(withRollSystem(withEditors(edition)));
@@ -1,12 +1,14 @@
1
1
  import { MidiFile } from "midifile-ts";
2
- export declare function midiTickToMilliseconds(ticks: number, microsecondsPerBeat: number, ppq: number): number;
2
+ import { Milliseconds } from "../Quantity";
3
+ export declare function midiTickToMilliseconds(ticks: number, microsecondsPerBeat: number, ppq: number): Milliseconds;
3
4
  interface Span<T extends string> {
4
5
  type: T;
5
6
  id: string;
7
+ /** In ticks of the file. */
6
8
  onset: number;
7
9
  offset: number;
8
- onsetMs: number;
9
- offsetMs: number;
10
+ onsetMs: Milliseconds;
11
+ offsetMs: Milliseconds;
10
12
  link?: string;
11
13
  }
12
14
  export interface NoteSpan extends Span<'note'> {
@@ -1,4 +1,5 @@
1
1
  import { MIDIControlEvents } from "midifile-ts";
2
+ import { milliseconds } from "../Quantity";
2
3
  const isNoteOn = (event) => event.type === 'channel' && event.subtype === 'noteOn';
3
4
  const isNoteOff = (event) => event.type === 'channel' && event.subtype === 'noteOff';
4
5
  const isPedalOn = (event) => (event.type === 'channel'
@@ -23,7 +24,7 @@ const isSoftPedalOff = (event) => {
23
24
  };
24
25
  export function midiTickToMilliseconds(ticks, microsecondsPerBeat, ppq) {
25
26
  const beats = ticks / ppq;
26
- return (beats * microsecondsPerBeat) / 1000;
27
+ return milliseconds((beats * microsecondsPerBeat) / 1000);
27
28
  }
28
29
  export const asSpans = (file, readLinks = false) => {
29
30
  const resultingSpans = [];
@@ -64,7 +65,7 @@ export const asSpans = (file, readLinks = false) => {
64
65
  pitch,
65
66
  channel: i,
66
67
  onsetMs,
67
- offsetMs: 0,
68
+ offsetMs: milliseconds(0),
68
69
  link
69
70
  });
70
71
  }
@@ -75,7 +76,7 @@ export const asSpans = (file, readLinks = false) => {
75
76
  onset: currentTime,
76
77
  offset: 0,
77
78
  onsetMs,
78
- offsetMs: 0,
79
+ offsetMs: milliseconds(0),
79
80
  link
80
81
  });
81
82
  }
@@ -1,4 +1,5 @@
1
1
  import { RollCopy } from "../RollCopy";
2
+ import { FeetPerMinute, Millimeters, Seconds, Track } from "../Quantity";
2
3
  /**
3
4
  * How a MIDI key number in one of Spencer Chase's roll files names a
4
5
  * tracker bar track.
@@ -14,13 +15,13 @@ import { RollCopy } from "../RollCopy";
14
15
  * approximate. Settling it needs a Spencer file whose expression holes
15
16
  * can be checked against the roll, hence the option to override.
16
17
  */
17
- export declare const spencerTrackOf: (pitch: number) => number;
18
+ export declare const spencerTrackOf: (pitch: number) => Track;
18
19
  /**
19
20
  * Spencer Chase's rolls seem to be scanned at a roll speed of
20
21
  * 83 (=8.3 feet per minute). A scanner feeds the paper at one
21
22
  * speed, so time in his files is proportional to place.
22
23
  */
23
- export declare const SPENCER_FEET_PER_MINUTE = 8.3;
24
- /** Place on the roll in mm after `seconds` at a constant `feetPerMinute`. */
25
- export declare const atConstantSpeed: (feetPerMinute: number) => (seconds: number) => number;
26
- export declare function readFromSpencerMIDI(midiBuffer: ArrayBuffer, placeAt?: (seconds: number) => number, trackOf?: (pitch: number) => number): RollCopy;
24
+ export declare const SPENCER_FEET_PER_MINUTE: import("..").Quantity<"ft/min">;
25
+ /** Place on the roll after `time` at a constant `speed`. */
26
+ export declare const atConstantSpeed: (speed: FeetPerMinute) => (time: Seconds) => Millimeters;
27
+ export declare function readFromSpencerMIDI(midiBuffer: ArrayBuffer, placeAt?: (time: Seconds) => Millimeters, trackOf?: (pitch: number) => Track): RollCopy;
@@ -1,6 +1,7 @@
1
1
  import { v4 } from "uuid";
2
2
  import { read } from "midifile-ts";
3
3
  import { asSpans } from "./midiSpans";
4
+ import { feetPerMinute, inSeconds, mm, track } from "../Quantity";
4
5
  /**
5
6
  * How a MIDI key number in one of Spencer Chase's roll files names a
6
7
  * tracker bar track.
@@ -17,18 +18,19 @@ import { asSpans } from "./midiSpans";
17
18
  * can be checked against the roll, hence the option to override.
18
19
  */
19
20
  export const spencerTrackOf = (pitch) => {
20
- const track = pitch - 13;
21
- return track < 10 ? track - 2 : track;
21
+ const position = pitch - 13;
22
+ return track(position < 10 ? position - 2 : position);
22
23
  };
23
24
  const MM_PER_FOOT = 304.8;
25
+ const SECONDS_PER_MINUTE = 60;
24
26
  /**
25
27
  * Spencer Chase's rolls seem to be scanned at a roll speed of
26
28
  * 83 (=8.3 feet per minute). A scanner feeds the paper at one
27
29
  * speed, so time in his files is proportional to place.
28
30
  */
29
- export const SPENCER_FEET_PER_MINUTE = 8.3;
30
- /** Place on the roll in mm after `seconds` at a constant `feetPerMinute`. */
31
- export const atConstantSpeed = (feetPerMinute) => (seconds) => feetPerMinute * MM_PER_FOOT / 60 * seconds;
31
+ export const SPENCER_FEET_PER_MINUTE = feetPerMinute(8.3);
32
+ /** Place on the roll after `time` at a constant `speed`. */
33
+ export const atConstantSpeed = (speed) => (time) => mm(speed * MM_PER_FOOT / SECONDS_PER_MINUTE * time);
32
34
  export function readFromSpencerMIDI(midiBuffer, placeAt = atConstantSpeed(SPENCER_FEET_PER_MINUTE), trackOf = spencerTrackOf) {
33
35
  const features = asSpans(read(midiBuffer))
34
36
  .filter(span => span.type === 'note')
@@ -40,8 +42,8 @@ export function readFromSpencerMIDI(midiBuffer, placeAt = atConstantSpeed(SPENCE
40
42
  unit: 'track'
41
43
  },
42
44
  horizontal: {
43
- from: placeAt(span.onsetMs / 1000),
44
- to: placeAt(span.offsetMs / 1000),
45
+ from: placeAt(inSeconds(span.onsetMs)),
46
+ to: placeAt(inSeconds(span.offsetMs)),
45
47
  unit: 'mm'
46
48
  }
47
49
  }));
@@ -1,12 +1,13 @@
1
1
  import { RollCopy } from "../RollCopy";
2
2
  import { TrackerBar } from "../TrackerBar";
3
+ import { Track } from "../Quantity";
3
4
  export interface StanfordAtonOptions {
4
5
  /**
5
6
  * Added to the scanning software's hole numbering to reach the
6
7
  * tracker bar. Left out, it is inferred by putting the rewind
7
8
  * perforation on the bar's rewind track.
8
9
  */
9
- trackShift?: number;
10
+ trackShift?: Track;
10
11
  bar?: TrackerBar;
11
12
  /**
12
13
  * Where the scan the analysis was made from can be seen. Stanford's
@@ -1,8 +1,9 @@
1
1
  import { v4 } from "uuid";
2
2
  import { AtonParser } from "./AtonParser";
3
3
  import { welteT100 } from "../systems/welteT100/bar";
4
+ import { inMillimeters, mean, mm, px, subtract, track } from "../Quantity";
4
5
  /** Values in these files carry their unit as a suffix, e.g. "37.7646px". */
5
- const px = (value) => parseFloat(value);
6
+ const readPx = (value) => px(parseFloat(value));
6
7
  /** The parser writes one record as an object and several as an array. */
7
8
  const listOf = (value) => value === undefined ? [] : Array.isArray(value) ? value : [value];
8
9
  /**
@@ -16,9 +17,8 @@ const chainedBadHoles = (holes, calibration) => holes
16
17
  .filter(hole => hole.NOTE_ATTACK && hole.OFF_TIME)
17
18
  .map(hole => ({
18
19
  ...hole,
19
- TRACKER_HOLE: `${Math.round((px(hole.CENTROID_COL) - calibration.offset) / calibration.separation)}`
20
+ TRACKER_HOLE: `${Math.round((readPx(hole.CENTROID_COL) - calibration.offset) / calibration.separation)}`
20
21
  }));
21
- const millimeters = (pixels, dpi) => pixels / dpi * 25.4;
22
22
  const median = (values) => {
23
23
  const sorted = [...values].sort((a, b) => a - b);
24
24
  const middle = Math.floor(sorted.length / 2);
@@ -32,7 +32,8 @@ const mostFrequent = (values) => {
32
32
  * Only the first hole of a chain carries an attack and an off time;
33
33
  * the rest continue it. The rewind perforation carries neither, and it
34
34
  * is the last thing punched on the roll, so whatever the holes past the
35
- * final musical attack sit on is the rewind track.
35
+ * final musical attack sit on is the rewind track, in the scanner's
36
+ * own numbering.
36
37
  */
37
38
  const rewindTrackIn = (holes) => {
38
39
  const lastMusical = holes.findLastIndex(hole => hole.NOTE_ATTACK);
@@ -46,16 +47,14 @@ const rewindTrackIn = (holes) => {
46
47
  */
47
48
  const gridOffsetOf = (holes, separation, stated) => {
48
49
  if (stated !== undefined)
49
- return px(stated);
50
- return median(holes.map(hole => px(hole.CENTROID_COL) - +hole.TRACKER_HOLE * separation));
50
+ return readPx(stated);
51
+ return px(median(holes.map(hole => readPx(hole.CENTROID_COL) - +hole.TRACKER_HOLE * separation)));
51
52
  };
52
53
  const punchDiameterOf = (holes, dpi) => {
53
54
  const circular = holes
54
- .filter(hole => px(hole.CIRCULARITY) > 0.95)
55
- .map(hole => millimeters(px(hole.PERIMETER), dpi) / Math.PI);
56
- if (!circular.length)
57
- return undefined;
58
- return circular.reduce((sum, diameter) => sum + diameter, 0) / circular.length;
55
+ .filter(hole => parseFloat(hole.CIRCULARITY) > 0.95)
56
+ .map(hole => mm(inMillimeters(readPx(hole.PERIMETER), dpi) / Math.PI));
57
+ return circular.length > 0 ? mean(circular) : undefined;
59
58
  };
60
59
  /**
61
60
  * The image service Stanford keeps for a scan, and a crop of a
@@ -85,12 +84,12 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
85
84
  const holes = json.ROLLINFO.HOLES.HOLE;
86
85
  const druid = json.ROLLINFO.DRUID;
87
86
  const stanford = druid ? stanfordScan(druid) : undefined;
88
- const separation = px(json.ROLLINFO.HOLE_SEPARATION);
87
+ const separation = readPx(json.ROLLINFO.HOLE_SEPARATION);
89
88
  const dpi = parseFloat(json.ROLLINFO.LENGTH_DPI);
90
89
  const measuredBy = measuredByOf(json.ROLLINFO);
91
90
  const rewindTrack = rewindTrackIn(holes);
92
91
  const shift = trackShift
93
- ?? (rewindTrack === undefined ? 0 : bar.rewindTrack - rewindTrack);
92
+ ?? (rewindTrack === undefined ? track(0) : track(bar.rewindTrack - rewindTrack));
94
93
  const calibration = {
95
94
  unit: 'px',
96
95
  offset: gridOffsetOf(holes, separation, json.ROLLINFO.HOLE_OFFSET),
@@ -100,27 +99,27 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
100
99
  const punchDiameter = punchDiameterOf(holes, dpi);
101
100
  const chains = [...holes, ...chainedBadHoles(listOf(json.ROLLINFO.BADHOLES?.HOLE), calibration)]
102
101
  .filter(hole => hole.NOTE_ATTACK && hole.OFF_TIME)
103
- .sort((a, b) => px(a.NOTE_ATTACK) - px(b.NOTE_ATTACK));
102
+ .sort((a, b) => readPx(a.NOTE_ATTACK) - readPx(b.NOTE_ATTACK));
104
103
  const features = chains
105
104
  .map((hole) => {
106
- const attack = px(hole.NOTE_ATTACK);
107
- const release = px(hole.OFF_TIME);
108
- const column = px(hole.ORIGIN_COL);
109
- const columnWidth = px(hole.WIDTH_COL);
105
+ const attack = readPx(hole.NOTE_ATTACK);
106
+ const release = readPx(hole.OFF_TIME);
107
+ const column = readPx(hole.ORIGIN_COL);
108
+ const columnWidth = readPx(hole.WIDTH_COL);
110
109
  return {
111
110
  type: 'Hole',
112
111
  id: v4(),
113
112
  ...(stanford && {
114
- depiction: stanford.depictionOf(column, attack, columnWidth, release - attack)
113
+ depiction: stanford.depictionOf(column, attack, columnWidth, subtract(release, attack))
115
114
  }),
116
115
  vertical: {
117
- from: +hole.TRACKER_HOLE + shift,
116
+ from: track(+hole.TRACKER_HOLE + shift),
118
117
  unit: 'track'
119
118
  },
120
119
  horizontal: {
121
120
  unit: 'mm',
122
- from: millimeters(attack, dpi),
123
- to: millimeters(release, dpi)
121
+ from: inMillimeters(attack, dpi),
122
+ to: inMillimeters(release, dpi)
124
123
  }
125
124
  };
126
125
  });
@@ -134,8 +133,8 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
134
133
  ...((scan ?? stanford) && { scan: scan ?? stanford?.scan }),
135
134
  measurements: {
136
135
  dimensions: {
137
- width: millimeters(px(json.ROLLINFO.ROLL_WIDTH), dpi),
138
- height: millimeters(px(json.ROLLINFO.IMAGE_LENGTH), dpi),
136
+ width: inMillimeters(readPx(json.ROLLINFO.ROLL_WIDTH), dpi),
137
+ height: inMillimeters(readPx(json.ROLLINFO.IMAGE_LENGTH), dpi),
139
138
  unit: 'mm'
140
139
  },
141
140
  ...(punchDiameter !== undefined && {
@@ -146,8 +145,8 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
146
145
  unit: 'px'
147
146
  },
148
147
  margins: {
149
- treble: px(json.ROLLINFO.HARD_MARGIN_TREBLE),
150
- bass: px(json.ROLLINFO.HARD_MARGIN_BASS),
148
+ treble: readPx(json.ROLLINFO.HARD_MARGIN_TREBLE),
149
+ bass: readPx(json.ROLLINFO.HARD_MARGIN_BASS),
151
150
  unit: 'px'
152
151
  },
153
152
  trackCalibration: calibration,