Skip to content

Instantly share code, notes, and snippets.

@scornork
Last active December 22, 2025 05:04
Show Gist options
  • Select an option

  • Save scornork/0206437dd3cf6d7da64e82a4b6fe8cb6 to your computer and use it in GitHub Desktop.

Select an option

Save scornork/0206437dd3cf6d7da64e82a4b6fe8cb6 to your computer and use it in GitHub Desktop.
A modern vanilla ES6+ implementation of a sinusoid approximated from a polynomial expansion of n terms
/**
* @author scornork <22217332+scornork@users.noreply.github.com>
* @see https://gist.github.com/scornork/0206437dd3cf6d7da64e82a4b6fe8cb6
*
* @typedef {Object} SinusoidTraits
* @property {number} [sampleRate=44100] - The overall wave sampling rate in Hz.
* @property {number} [frequency=1000] - The oscillation wave frequency in Hz.
* @property {number} [length=1024] - The total wave samples / buffer size.
* @property {number} [terms=13] - The total polynomial expansion terms.
*
* @typedef {Object} PolynomialApproximation
* @property {number} termPartialSum - The current running accumulation for the
* iterative reduction performed to approximate the Maclaurin power
* series expansion.
* @property {number} priorTermValue - The value of the term before to consider
* and use in the accumulative computation of the current term.
*
* @typedef {Object} SinusoidTraitsInstance
* @property {Map<keyof SinusoidTraits, number>} schema - The reference used to
* validate user-defined traits or as a fill-in for any invalid or
* absent ones.
* @property {Readonly<SinusoidTraits>} traits - The consolidated list
* of valid sinusoidal traits defined by the user or by presets in
* the schema to use in the approximation of sinudoidal waveform.
*/
/**
* @description A modern vanilla ES6+ implementation of a sinusoid approximated
* from a polynomial expansion of n terms.
*
* @example
*
* Approximate.waveform({
* // Pass sinusoidal traits to define the final sinusoidal waveform, or just
* // leave this blank to use in-built defaults.
.
* sampleRate: 44100,
* frequency : 22050,
* length : 2048,
* terms : 13
* })
* .then(amplitudes => (
* console.log(amplitudes),
* amplitudes
* ))
* .catch(error => console.error(
* error.stack
* ));
*/
const Approximate = Object.freeze((_ => {
/**
* @description - A configurator to validate and consolidate usable traits to
* be used in the power series expansion by providing a schema
* to check against and fallback on.
*
* @param {Partial<SinusoidTraits>} [traits={}] - User-defined traits to be
* assessed and applied as an override to the relevant default.
* @returns ({
* traits : Readonly<SinusoidTraits>,
* schema : Map<string, number>,
* _traits: Readonly<SinusoidTraits> | null,
* _schema: Map<string, number> | null
* }) An object that lazily evaluates user-defined traits against
* an internal schema, merging only those well-defined with an
* acceptable value that differs from existing schema presets,
* if any had been passed at all to consider. Traits deemed to
* be absent or invalid would otherwise default to the presets
* held by the same schema.
*/
const Sinusoidal = (traits = {}) => ({
get traits () {
return this._traits ??= !!traits && Object.getPrototypeOf(traits) &&
Object.prototype && Object.entries(traits).some(([trait, given]) =>
(typeof given === "string" || Number.isFinite(given)) && trait &&
typeof trait === "string" && this.schema.has(trait) &&
given !== this.schema.get(trait))
? Object.fromEntries(Array.from(this.schema.entries())
.map(([trait, preset]) => [
trait,
!!traits?.[trait] && trait !== preset &&
typeof trait === "string" && !!trait &&
Object.getPrototypeOf(traits[trait]) ===
Object.getPrototypeOf(preset)
? traits[trait]
: preset
])
)
: Object.fromEntries(this.schema.entries());
},
/**
* @description The sinusoidal traits schema which also doubles up defaults
* to fallback on for absent or invalid traits.
*
* @returns {Map<string, number>} The valid sinusoidal traits required when
* approximating the Maclaurin power series expansion.
*/
get schema () {
return this._schema ??= new Map([
["sampleRate", 44100],
["frequency" , 22050],
["length" , 2048],
["terms" , 13]
])
},
_schema: null,
_traits: null
});
/**
* @description A factory to approximate the sinusoidal waveform by computing
* a polynomial expansion with an iterative reduction function.
*
* @param {Partial<SinusoidTraits>} [traits={}] - An object with user-defined
* traits to be validated against a predefined schema. Validated
* ones will subsequently override the existing defaults held by
* the same schema.
*
* @returns ({
* currentPhaseAngleOf: (sampleIndex: number) => number,
* safelyWrapPrincipal: (phaseAngle: number) => number,
* powerSeriesExpanded: (
* bufferSize: number,
* totalTerms: number
* ) => Float64Array,
* _phaseDeltaPerSample: number | null,
* _fullRotationalAngle: number | null,
* _waveform : Float64Array | null,
* _sinusoid : SinusoidTraitsInstance | null
* }) An object that approximates the polynomial expansion from a
* consolidation of usuable sinusoidal traits.
*/
const Polynomial = (traits = {}) => ({
/**
* @description - Calculates the phase angle of a sample at the position of
* its specified index using the predetermined angular phase
* delta.
*
* @param {number} sampleIndex - The current sequential order of the sample.
*
* @returns {number} The current phase angle in radians.
*/
currentPhaseAngleOf (sampleIndex) {
return (this.phaseDeltaPerSample * sampleIndex)
% this.fullRotationalAngle;
},
/**
* @description - Normalises the phase angle to the principal range (-π, π]
* to preserve floating point precision as much as possible.
*
* @param {number} phaseAngle - The phase angle in radians to wrap around.
*
* @returns {number} The safely wrapped phase angle of the principal branch.
*/
safelyWrapPrincipal (phaseAngle) {
return phaseAngle > Math.PI
? phaseAngle - this.fullRotationalAngle
: phaseAngle < -Math.PI
? phaseAngle + this.fullRotationalAngle
: phaseAngle;
},
/**
* @description - Derives a sinusoidal waveform from the approximation of a
* Maclaurin power series expansion that iteratively reduces
* an accumulation of the polynomial terms without expensive
* tail-call recursions.
*
* @param {number} [bufferSize=this.sinusoid.traits.length] - The sizing of
* of the buffer to hold the total range of expected samples.
* @param {number} [totalTerms=this.sinusoid.traits.terms] - The amount of
* terms to approximate the Maclaurin power series expansion.
*
* @returns {Float64Array} A typed array containing the resulting amplitude
* values.
*/
powerSeriesExpanded (
bufferSize = this.sinusoid.traits.length,
totalTerms = this.sinusoid.traits.terms
) {
return Float64Array.from({ length: bufferSize }, (_, sampleIndex) => {
const wrappedPhaseAngle = this.safelyWrapPrincipal(
this.currentPhaseAngleOf(
sampleIndex
)
);
/** @type {PolynomialApproximation} */
const approximated = Array.from({ length: totalTerms }, (_, term) =>
++term).reduce((accumulated, termCount) => {
const currentTermValue = (accumulated.priorTermValue * -1
* wrappedPhaseAngle ** 2)
/ ((2 * termCount + 1)
* (2 * termCount));
return {
termPartialSum: accumulated.termPartialSum + currentTermValue,
priorTermValue: currentTermValue,
};
}, {
termPartialSum: wrappedPhaseAngle,
priorTermValue: wrappedPhaseAngle,
});
return approximated.termPartialSum;
});
},
/**
* @description - Calculates the change in phase angle per sample based its
* underlying sinusoidal frequency and sampleRate traits.
*
* @returns {number} The angular phase change per sample in radians.
*/
get phaseDeltaPerSample () {
return this._phaseDeltaPerSample ??= (this.fullRotationalAngle
* this.sinusoid.traits.frequency)
/ this.sinusoid.traits.sampleRate;
},
/**
* @description - Provides the total radians of a full rotation in radians.
*
* @returns {number} Two times the value of π.
*/
get fullRotationalAngle () {
return this._fullRotationalAngle ??= Math.PI * 2;
},
/**
* @description - A lazy initiation of the operations that will approximate
* a sinusoidal waveform from a Maclaurin power series.
*
* @returns {Float64Array} The buffer with the approximated amplitudes in a
* sinusoidal waveform.
*/
get waveform () {
return this._waveform ??= this.powerSeriesExpanded();
},
/**
* @description - The sinusoidal traits instance to validate or consolidate
* the properties required in the approximation of the power
* series expansion.
*
* @returns {SinusoidTraitsInstance} An instance of sinusoidal traits to be
* used in the approximation of the waveform.
*/
get sinusoid () {
return this._sinusoid ??= Sinusoidal(traits);
},
_fullRotationalAngle: null,
_phaseDeltaPerSample: null,
_sinusoid : null,
_waveform : null
});
return Object.freeze({
/**
* @description Approximates a sinusoid based on given traits and returns a
* a Promise to deliberately block the thread to focus compute
* here in order to complete the calculations quickly. Promise
* can therefore be removed without breaking anything.
*
* @param {Partial<SinusoidTraits>} [traits] - For convenient customisation
* of the sample rate, frequency, length, or terms to apply to
* the approximation. Leave blank at invocation to use default
* instead.
*
* @returns {Promise<Float64Array>} A promise resolving to the approximated
* sinusoidal amplitudes.
*/
waveform (traits = Sinusoidal().traits) {
return Promise.try(_ => Polynomial(traits).waveform);
}
});
})());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment