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.
- package/lib/Agent.d.ts +19 -2
- package/lib/Agent.js +10 -1
- package/lib/Collation.d.ts +7 -11
- package/lib/Collation.js +4 -3
- package/lib/Edition.d.ts +8 -1
- package/lib/EditionView.js +12 -11
- package/lib/Emulation.d.ts +3 -2
- package/lib/Emulation.js +14 -18
- package/lib/Feature.d.ts +5 -4
- package/lib/Quantity.d.ts +61 -0
- package/lib/Quantity.js +25 -0
- package/lib/ReproducingSystem.d.ts +15 -12
- package/lib/RollCopy.d.ts +23 -54
- package/lib/RollCopy.js +8 -8
- package/lib/TrackCalibration.d.ts +10 -9
- package/lib/TrackCalibration.js +6 -5
- package/lib/TrackerBar.d.ts +14 -8
- package/lib/TrackerBar.js +12 -11
- package/lib/alignment.d.ts +3 -2
- package/lib/alignment.js +24 -32
- package/lib/editionOps.js +5 -2
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/migrate.js +5 -1
- package/lib/readers/midiSpans.d.ts +5 -3
- package/lib/readers/midiSpans.js +4 -3
- package/lib/readers/spencerMidi.d.ts +6 -5
- package/lib/readers/spencerMidi.js +9 -7
- package/lib/readers/stanfordAton.d.ts +2 -1
- package/lib/readers/stanfordAton.js +25 -26
- package/lib/schema.json +289 -109
- package/lib/spec/context.json +1 -0
- package/lib/systems/welteT100/bar.js +2 -1
- package/lib/systems/welteT100/system.d.ts +8 -7
- package/lib/systems/welteT100/system.js +16 -13
- package/package.json +1 -1
package/lib/TrackCalibration.js
CHANGED
|
@@ -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 = (
|
|
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
|
|
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
|
};
|
package/lib/TrackerBar.d.ts
CHANGED
|
@@ -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:
|
|
15
|
-
readonly to:
|
|
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
|
|
39
|
-
readonly width:
|
|
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:
|
|
52
|
+
readonly rewindTrack: Track;
|
|
52
53
|
/** `undefined` for a position the bar does not read. */
|
|
53
|
-
meaningOf(
|
|
54
|
+
meaningOf(position: Track): TrackMeaning | undefined;
|
|
54
55
|
/** `undefined` for a position the bar does not read. */
|
|
55
|
-
roleOf(
|
|
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:
|
|
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 = (
|
|
15
|
-
const meaningOf = (
|
|
16
|
-
const role = roleOf(
|
|
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:
|
|
23
|
+
pitch: position - spec.notes.from + spec.notes.lowestPitch
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
|
-
const expressionType = spec.expressions.get(
|
|
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
|
|
31
|
+
const rewind = [...spec.expressions]
|
|
31
32
|
.find(([, type]) => type === 'Rewind')?.[0];
|
|
32
|
-
if (
|
|
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
|
};
|
package/lib/alignment.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
13
|
-
shift:
|
|
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
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
|
|
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
|
|
53
|
-
if (!copy.ops.includes('stretched') || !
|
|
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
|
|
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 ||
|
|
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
package/lib/index.js
CHANGED
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
|
-
|
|
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
|
-
|
|
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:
|
|
9
|
-
offsetMs:
|
|
10
|
+
onsetMs: Milliseconds;
|
|
11
|
+
offsetMs: Milliseconds;
|
|
10
12
|
link?: string;
|
|
11
13
|
}
|
|
12
14
|
export interface NoteSpan extends Span<'note'> {
|
package/lib/readers/midiSpans.js
CHANGED
|
@@ -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) =>
|
|
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
|
|
24
|
-
/** Place on the roll
|
|
25
|
-
export declare const atConstantSpeed: (
|
|
26
|
-
export declare function readFromSpencerMIDI(midiBuffer: ArrayBuffer, placeAt?: (
|
|
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
|
|
21
|
-
return track < 10 ?
|
|
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
|
|
31
|
-
export const atConstantSpeed = (
|
|
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
|
|
44
|
-
to: placeAt(span.offsetMs
|
|
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?:
|
|
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
|
|
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((
|
|
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
|
|
50
|
-
return median(holes.map(hole =>
|
|
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 =>
|
|
55
|
-
.map(hole =>
|
|
56
|
-
|
|
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 =
|
|
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) =>
|
|
102
|
+
.sort((a, b) => readPx(a.NOTE_ATTACK) - readPx(b.NOTE_ATTACK));
|
|
104
103
|
const features = chains
|
|
105
104
|
.map((hole) => {
|
|
106
|
-
const attack =
|
|
107
|
-
const release =
|
|
108
|
-
const column =
|
|
109
|
-
const columnWidth =
|
|
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
|
|
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:
|
|
123
|
-
to:
|
|
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:
|
|
138
|
-
height:
|
|
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:
|
|
150
|
-
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,
|