@arghotuning/arghotun
v0.9.3
Published
JavaScript (TypeScript) Parsing & Serialization for Argho Tuning (*.arghotun) Files
Downloads
48
Readme
arghotun-js
IMPORTANT: This is a pre-release version of an upcoming open source library for working with musical tuning systems. It is NOT yet ready for external usage, and many of the linked websites and projects are not yet publicly available.
This is the canonical JavaScript (TypeScript) implementation for parsing &
serializing Argho Tuning files (*.arghotun
). See the
Argho Tuning website or
official spec for more background.
It is published as the @arghotuning/arghotun NPM package including TypeScript type definition files, so it can be used by both TypeScript and plain JavaScript projects.
This library is designed for compatibility with both web browser and Node.js server/tool environments.
Usage
Add this dependency to your project, using your package manager. With NPM:
npm install @arghotuning/arghotun
To parse an Argho Tuning file from a string:
import {
ArghoTuningContext,
defaultArghotunTranslations,
TuningJsonParser,
} from '@arghotuning/arghotun';
const arghoContext = new ArghoTuningContext({
translations: defaultArghotunTranslations(),
});
const parser = new TuningJsonParser(arghoContext);
try {
const parseResult = parser.parseJsonString(inputJsonString);
if (parseResult.warnings.length >= 1) {
// Some file content wasn't understood. Display warnings[] to user that
// this tuning may not have been interpreted fully.
}
const tuning = parseResult.tuning;
} catch (err) {
// Input file was invalid; show err.message to user...
}
To output a human-readable Argho Tuning file to a string:
import {TuningJsonSerializer} from 'arghotun';
const serializer = new TuningJsonSerializer(arghoContext);
const outputJsonString = serializer.serializeToString(tuning);
Tuning Object Format
The provided types and methods correspond fairly closely to the object structure of the JSON format.
Tuning
Initialize a new default N-tone equal temperament tuning, with root pitch of middle C (MIDI note 60), a global tune of A4 = 440.0 Hz, and a 1:1 key mapping:
const quarterToneTuning =
Tuning.defaultNtet(arghoContext, 'My 24-TET Tuning', 24);
const defaultTuning =
Tuning.default12tet(arghoContext, 'My 12-TET Tuning');
Access or update tuning-level metadata:
const metadata = tuning.getMetadata(); // See below.
Get the Scale
and KeyMapping
(both are mutable):
const scale = tuning.getScale();
const mapping = tuning.getMapping();
Change the # of scale degrees or reset the scale and mapping. Note that add and remove are two separately named operations to emphasize their very different semantics:
// Add 3 additional degrees; each defaults to a 1:1 ratio (unison)
// measured from the root, without affecting any existing scale
// degrees. Newly added degrees are NOT mapped.
scale.addDegreesToEnd(3);
// Remove the last (by scale degree index) 3 degrees from the end
// of the scale. Any remaining scale degrees that were measured
// from the removed degrees will be updated to "skip over" the
// removed degrees, while preserving their existing tunings. Any
// input keys that were mapping to removed scale degrees are set
// to unmapped, leaving keySpan unchanged.
scale.removeDegreesFromEnd(3);
// Reset scale and mapping back to a 24-tone equal temperament
// scale and 1:1 key mapping, keeping the currently configured
// scale root and mapping root MIDI pitch. Also does not change
// other existing metadata (e.g. name, description).
scale.resetToDefaultNtet(24);
TuningMetadata
Mutable object for the tuning's name, description, and accidental display preference.
const name = metadata.getName();
metadata.setName('Updated Tuning Name');
const description = metadata.getDescription(); // '' if none.
metadata.setDescription('Description of the tuning');
const accidentalPref = metadata.getDisplayAccidentalsAs();
if (accidentalPref === AccidentalDisplayPref.SHARPS) {
// Display any pitch names that need accidentals with sharps...
} else {
// Display any pitch names that need accidentals with flats...
}
metadata.setDisplayAccidentalsAs(AccidentalDisplayPref.FLATS);
Scale
Access metadata (note that changing the # of degrees must be performed on the
parent Tuning
object):
const numDegrees = scale.getNumDegrees();
const octavesSpanned = scale.getOctavesSpanned(); // See spec for details.
Access or update the (sounding) scale root:
const root = scale.getRoot(); // Immutable.
scale.setRoot(updatedRoot); // Replaces old value.
Access or update an upper scale degree tuning or how it is measured. Remember
that indexes are 0-based (but should be displayed to the user as 1-based).
Changing measureFromIndex
preserves the sounding tuning:
const deg3 = scale.getUpperDegree(2); // Immutable.
const index = deg3.scaleDegreeIndex; // 2.
const measureFromIndex = deg3.measureFromIndex;
const interval = deg3.tunedInterval;
// Replace old TunedInterval:
scale.setUpperDegreeTuning(2, updatedTunedInterval);
// Measure 3rd scale degree from 6th degree:
scale.setMeasureFromIndex(2, 5);
Because the minimum and maximum valid tuned intervals for a given upper degree
depend on the current scale configuration and are complex to compute, this
library also provides getTuningLimitsForUpperDegree()
, which can be used to
limit an input interval to within valid range before calling
setUpperDegreeTuning()
.
const limits = scale.getTuningLimitsForUpperDegree(2);
scale.setUpperDegreeTuning(2, limits.ensureWithin(userInputInterval));
Measure the interval between any two scale degrees (either can also be 0
for
the root):
const intervalFromDeg3ToRoot = scale.getIntervalFromSourceToDest(2, 0);
TunedInterval
Immutable value that measures an interval from a source pitch to a destination pitch, specified either in cents or as a frequency ratio. A negative cents value or a ratio less than 1 represents a downward interval.
Create a new immutable interval value:
const up123Dot45Cents = TunedInterval.fromCents(arghoContext, 123.45);
const downPure5th = TunedInterval.fromRatio(arghoContext, 2, 3);
Access cents or frequency ratio tuning:
if (interval.getSpecType() === TunedIntervalSpecType.CENTS) {
// User specified interval in cents...
} else {
// User specified interval as a frequency ratio...
}
// Regardless of how it was specified, all of these accessors will always
// yield values:
const cents = interval.getCents();
const numerator = interval.getRatioNumerator();
const denominator = interval.getDenominator(); // Defaults to 1.
Invert or add intervals:
const upPure5th = downPure5th.inverse(); // Returns new immutable value.
const upPure9th = upPure5th.plus(upPure5th); // (Ditto).
This library also provides the utility functions minInterval()
and
maxInterval()
for selecting the minimum (largest downward or smallest upward)
and maximum (largest upward or smallest downward) of two intervals.
RootScaleDegree
Create default sounding root pitch (12-TET middle C, with global tune A4 = 440.0 Hz):
const defaultMiddleCRoot = RootScaleDegree.default(arghoContext);
Create custom souding root pitch, specified either by exact sounding frequency or relative to the closest 12-tone equal temperament tuned pitch. In either case, frequencies are still considered relative to the given global tuning (so if the global tuning is later changed, this sounding root pitch should be adjusted by the same amount):
const tuningA4Hz = 442.0;
const root234Dot5Hz =
RootScaleDegree.exact(arghoContext, tuningA4Hz, 234.5);
const root12Dot3CentsBelowMiddleC =
RootScaleDegree.relative(arghoContext, tuningA4Hz, 60, -12.3);
Access values (all immutable):
const globalTuneA4Hz = root.getGlobalTuneA4Hz();
if (root.getSpecType() === RootScaleDegreeSpecType.EXACT_FREQ) {
// User specified root as an exact sounding frequency in Hz...
} else {
// User specified root as cents +/- offset relative to 12-TET pitch...
}
// Regardless of how it was specified, all of these accessors will always
// yield values:
const rootFreqHz = root.getFreqHz();
const nearestMidiPitch = root.getNearestMidiPitch();
const centsFromNearestMidiPitch = root.getCentsFrom12tet();
KeyMapping
Access or update the # of keys in the mapping:
const keySpan = mapping.getKeySpan();
mapping.setKeySpan(24); // Any new keys start out unmapped.
Access or update mapped root MIDI pitch (which will sound the configured scale's root degree).
const mappingRootMidiPitch = mapping.getRootMidiPitch();
// Set to mapping root to D above middle C. Doesn't affect scale.
mapping.setRootMidiPitch(62);
Access or update mapped scale degrees:
const mappedDegreeIndex = mapping.getMappedScaleDegreeIndexOrNull(5);
if (mappedDegreeIndex == null) {
// Key isn't mapped to anything (don't produce any sound)...
} else {
// Mapped to a scale degree...
}
// When 6th key is played, play 3rd scale degree.
mapping.setMappedScaleDegreeIndex(5, 2);
// When 4th key is played, play the scale root.
mapping.setMappedScaleDegreeIndex(3, 0);
// When 3rd key is played, don't play any scale degree (unmap).
mapping.setMappedScaleDegree(2, null);
Implementing Tuning Support in Your Instrument
For virtual instrument developers, a very simple way to use a loaded Tuning
is
via KeyToSoundMap
:
const keyToSoundMap = KeyToSoundMap.calcFor(arghoContext, tuning);
function handleNoteOn(inputMidiPitch) {
if (keyToSoundMap.isMapped(inputMidiPitch)) {
const soundingScaleDegree = keyToSoundMap.mappedSoundFor(inputMidiPitch);
playSoundWith(soundingScaleDegree.freqHz);
// Can also access, if needed:
// - soundingScaleDegree.scaleDegreeIndex
// - soundingScaleDegree.octaveOffset (# octaves sounding away from "main" scale)
} else {
// Unmapped: drop this note (no output scale degree for it).
}
}
Validation, Canonicalization, and Helper Utilities
For applications that allow the user to edit tuning fields, this library provides constants and utility functions:
ArghoTuningLimits
: numerical limits for min/max field values.canonicalizeName()
,canonicalizeDescription()
: tidy whitespace (before validation).strLengthUTF8Bytes()
: measure string length in bytes when encoded in UTF-8.ratioFromCents()
,centsFromRatio()
: convert between frequency ratios and cents.freq12tet()
,nearest12tetPitch()
: utilities for working with 12-tone equal temperament pitches and frequencies.
Translations and Internationalization (i18n)
Since the main user-visible strings this library produces are warnings and error messages, it is up to each client application project whether these need to be translated into languages other than English or not.
Currently, only English translations are included, but the library is set up to allow for simple translations to be provided for other locales.
Since client application projects likely will use different i18n frameworks, this library does not choose a specific framework to adopt; instead, it uses the lightweight interface from the simple-tr8n-js library, which can be used directly or easily adapted to work with any existing i18n framework.
See default-translations.ts for the default English strings.
Licenses
This is free open source software. The code in this project is made available
under the Apache-2.0 license. See the
ThirdPartyNotices/
folder for information about the software libraries this
project depends upon (not including development-only dependencies).
Source files and LICENSES/
are annotated and checked using
reuse.software tools.
Related Projects
Canonical implementations are available in many programming languages (including
TypeScript/JavaScript, C++, Lua/KSP) for parsing & serialization, applying
these tunings once loaded (Argho Engine
projects), common UI components, and
more. See the list of official projects.
TODO: Update link once it exists.