@arghotuning/arghotun
TypeScript icon, indicating that this package has built-in type declarations

0.9.3 • Public • Published

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:

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.

Readme

Keywords

none

Package Sidebar

Install

npm i @arghotuning/arghotun

Weekly Downloads

11

Version

0.9.3

License

Apache-2.0

Unpacked Size

257 kB

Total Files

147

Last publish

Collaborators

  • lindurion