Last active
December 22, 2025 05:04
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * @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