Skip to content

Commit 2875342

Browse files
committed
Decided to use the harmonics editor as the basis for the picked string instrument instead of the pulse width slider. It is more flexible, although I had to extend the rendered upper harmonics range for the initial pick sound.
1 parent 61e93a3 commit 2875342

File tree

3 files changed

+60
-105
lines changed

3 files changed

+60
-105
lines changed

editor/SongEditor.ts

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,6 @@ import {ChangeTempo, ChangeEchoDelay, ChangeEchoSustain, ChangeReverb, ChangeVol
253253
this._transitionRow,
254254
this._chordSelectRow,
255255
this._vibratoSelectRow,
256-
this._intervalSelectRow,
257256
this._chipWaveSelectRow,
258257
this._chipNoiseSelectRow,
259258
this._algorithmSelectRow,
@@ -263,9 +262,10 @@ import {ChangeTempo, ChangeEchoDelay, ChangeEchoSustain, ChangeReverb, ChangeVol
263262
this._spectrumRow,
264263
this._harmonicsRow,
265264
this._drumsetGroup,
266-
this._pulseEnvelopeRow,
267265
this._pulseWidthRow,
266+
this._pulseEnvelopeRow,
268267
this._stringSustainRow,
268+
this._intervalSelectRow,
269269
div({class: "selectRow"},
270270
span({class: "tip", onclick: ()=>this._openPrompt("effects")}, "Effects:"),
271271
div({class: "selectContainer"}, this._effectsSelect),
@@ -705,12 +705,18 @@ import {ChangeTempo, ChangeEchoDelay, ChangeEchoSustain, ChangeReverb, ChangeVol
705705
} else {
706706
this._spectrumRow.style.display = "none";
707707
}
708-
if (instrument.type == InstrumentType.harmonics) {
708+
if (instrument.type == InstrumentType.harmonics || instrument.type == InstrumentType.pickedString) {
709709
this._harmonicsRow.style.display = "";
710710
this._harmonicsEditor.render();
711711
} else {
712712
this._harmonicsRow.style.display = "none";
713713
}
714+
if (instrument.type == InstrumentType.pickedString) {
715+
this._stringSustainRow.style.display = "";
716+
this._stringSustainSlider.updateValue(instrument.stringSustain);
717+
} else {
718+
this._stringSustainRow.style.display = "none";
719+
}
714720
if (instrument.type == InstrumentType.drumset) {
715721
this._drumsetGroup.style.display = "";
716722
this._transitionRow.style.display = "none";
@@ -761,20 +767,11 @@ import {ChangeTempo, ChangeEchoDelay, ChangeEchoSustain, ChangeReverb, ChangeVol
761767
if (instrument.type == InstrumentType.pwm) {
762768
this._pulseEnvelopeRow.style.display = "";
763769
setSelectedValue(this._pulseEnvelopeSelect, instrument.pulseEnvelope);
764-
} else {
765-
this._pulseEnvelopeRow.style.display = "none";
766-
}
767-
if (instrument.type == InstrumentType.pickedString) {
768-
this._stringSustainRow.style.display = "";
769-
this._stringSustainSlider.updateValue(instrument.stringSustain);
770-
} else {
771-
this._stringSustainRow.style.display = "none";
772-
}
773-
if (instrument.type == InstrumentType.pwm || instrument.type == InstrumentType.pickedString) {
774770
this._pulseWidthRow.style.display = "";
775771
this._pulseWidthSlider.input.title = prettyNumber(getPulseWidthRatio(instrument.pulseWidth) * 100) + "%";
776772
this._pulseWidthSlider.updateValue(instrument.pulseWidth);
777773
} else {
774+
this._pulseEnvelopeRow.style.display = "none";
778775
this._pulseWidthRow.style.display = "none";
779776
}
780777

synth/SynthConfig.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ SOFTWARE.
223223
public static readonly drumsetBaseExpression: number = 0.45; // Drums tend to be loud but brief!
224224
public static readonly harmonicsBaseExpression: number = 0.025;
225225
public static readonly pwmBaseExpression: number = 0.04725; // It's actually closer to half of this, the synthesized pulse amplitude range is only .5 to -.5, but also note that the fundamental sine partial amplitude of a square wave is 4/π times the measured square wave amplitude.
226-
public static readonly pickedStringBaseExpression: number = 0.03;
226+
public static readonly pickedStringBaseExpression: number = 0.035; // Same as harmonics, but compensate for lacking the "interval" feature.
227227
public static readonly distortionBaseVolume: number = 0.0125; // Distortion is not affected by pitchDamping, which otherwise approximately halves expression for notes around the middle of the range.
228228
public static readonly bitcrusherBaseVolume: number = 0.0125; // Same as distortion, used when bit crushing is maxed out (aka "1-bit" output).
229229

@@ -393,6 +393,7 @@ SOFTWARE.
393393
public static readonly spectrumMax: number = (1 << Config.spectrumControlPointBits) - 1;
394394
public static readonly harmonicsControlPoints: number = 28;
395395
public static readonly harmonicsRendered: number = 64;
396+
public static readonly harmonicsRenderedForPickedString: number = 1 << 9; // 512
396397
public static readonly harmonicsControlPointBits: number = 3;
397398
public static readonly harmonicsMax: number = (1 << Config.harmonicsControlPointBits) - 1;
398399
public static readonly harmonicsWavelength: number = 1 << 11; // 2048

synth/synth.ts

Lines changed: 48 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
467467
public harmonics: number[] = [];
468468
private _wave: Float32Array | null = null;
469469
private _waveIsReady: boolean = false;
470+
private _generatedForType: InstrumentType;
470471

471472
constructor() {
472473
this.reset();
@@ -486,7 +487,13 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
486487
this._waveIsReady = false;
487488
}
488489

489-
public getCustomWave(): Float32Array {
490+
public getCustomWave(instrumentType: InstrumentType): Float32Array {
491+
if (this._generatedForType != instrumentType) {
492+
this._generatedForType = instrumentType;
493+
this._waveIsReady = false;
494+
}
495+
const harmonicsRendered: number = (instrumentType == InstrumentType.pickedString) ? Config.harmonicsRenderedForPickedString : Config.harmonicsRendered;
496+
490497
if (this._waveIsReady) return this._wave!;
491498

492499
const waveLength: number = Config.harmonicsWavelength;
@@ -504,11 +511,11 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
504511
const overallSlope: number = -0.25;
505512
let combinedControlPointAmplitude: number = 1;
506513

507-
for (let harmonicIndex: number = 0; harmonicIndex < Config.harmonicsRendered; harmonicIndex++) {
514+
for (let harmonicIndex: number = 0; harmonicIndex < harmonicsRendered; harmonicIndex++) {
508515
const harmonicFreq: number = harmonicIndex + 1;
509516
let controlValue: number = harmonicIndex < Config.harmonicsControlPoints ? this.harmonics[harmonicIndex] : this.harmonics[Config.harmonicsControlPoints - 1];
510517
if (harmonicIndex >= Config.harmonicsControlPoints) {
511-
controlValue *= 1 - (harmonicIndex - Config.harmonicsControlPoints) / (Config.harmonicsRendered - Config.harmonicsControlPoints);
518+
controlValue *= 1 - (harmonicIndex - Config.harmonicsControlPoints) / (harmonicsRendered - Config.harmonicsControlPoints);
512519
}
513520
const normalizedValue: number = controlValue / Config.harmonicsMax;
514521
let amplitude: number = Math.pow(2, controlValue - Config.harmonicsMax + 1) * Math.sqrt(normalizedValue);
@@ -540,42 +547,6 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
540547
}
541548
}
542549

543-
export class PickedStringImpulseWave {
544-
private static _wave: Float32Array | null = null;
545-
546-
private constructor () { throw new Error(); } // Don't instantiate.
547-
548-
public static getWave(): Float32Array {
549-
if (PickedStringImpulseWave._wave != null) return PickedStringImpulseWave._wave;
550-
551-
const waveLength: number = Config.harmonicsWavelength;
552-
PickedStringImpulseWave._wave = new Float32Array(waveLength + 1);
553-
const wave: Float32Array = PickedStringImpulseWave._wave;
554-
const retroWave: Float32Array = getDrumWave(0, null, null);
555-
556-
for (let harmonicFreq: number = 1; harmonicFreq < (waveLength >> 1); harmonicFreq++) {
557-
let amplitude: number = 0.5 / harmonicFreq; // The symmetric inverse FFT doubles amplitudes, compensating for that here.
558-
559-
// Multiply all the sine wave amplitudes by 1 or -1 based on the LFSR
560-
// retro wave (effectively random) to avoid egregiously tall spikes.
561-
amplitude *= retroWave[harmonicFreq + 588];
562-
563-
/*
564-
const radians: number = 0.61803398875 * harmonicFreq * harmonicFreq * Math.PI * 2.0;
565-
wave[harmonicFreq] = Math.sin(radians) * amplitude;
566-
wave[waveLength - harmonicFreq] = Math.cos(radians) * amplitude;
567-
*/
568-
wave[waveLength - harmonicFreq] = amplitude;
569-
}
570-
571-
inverseRealFourierTransform(wave, waveLength);
572-
performIntegral(wave);
573-
// The first sample should be zero, and we'll duplicate it at the end for easier interpolation.
574-
wave[waveLength] = wave[0];
575-
return wave;
576-
}
577-
}
578-
579550
export class FilterControlPoint {
580551
public freq: number = 0;
581552
public gain: number = Config.filterGainCenter;
@@ -894,7 +865,7 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
894865
break;
895866
case InstrumentType.pickedString:
896867
this.chord = Config.chords.dictionary["strum"].index;
897-
this.pulseWidth = Config.pulseWidthRange - 3;
868+
this.harmonicsWave.reset();
898869
this.stringSustain = 6;
899870
break;
900871
default:
@@ -966,6 +937,13 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
966937
instrumentObject["filterEnvelope"] = this.getFilterEnvelope().name; // DEPRECATED
967938
}
968939

940+
if (this.type == InstrumentType.harmonics || this.type == InstrumentType.pickedString) {
941+
instrumentObject["harmonics"] = [];
942+
for (let i: number = 0; i < Config.harmonicsControlPoints; i++) {
943+
instrumentObject["harmonics"][i] = Math.round(100 * this.harmonicsWave.harmonics[i] / Config.harmonicsMax);
944+
}
945+
}
946+
969947
if (this.type == InstrumentType.noise) {
970948
instrumentObject["wave"] = Config.chipNoises[this.chipNoise].name;
971949
} else if (this.type == InstrumentType.spectrum) {
@@ -1000,10 +978,6 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
1000978
} else if (this.type == InstrumentType.harmonics) {
1001979
instrumentObject["interval"] = Config.intervals[this.interval].name;
1002980
instrumentObject["vibrato"] = Config.vibratos[this.vibrato].name;
1003-
instrumentObject["harmonics"] = [];
1004-
for (let i: number = 0; i < Config.harmonicsControlPoints; i++) {
1005-
instrumentObject["harmonics"][i] = Math.round(100 * this.harmonicsWave.harmonics[i] / Config.harmonicsMax);
1006-
}
1007981
} else if (this.type == InstrumentType.fm) {
1008982
const operatorArray: Object[] = [];
1009983
for (const operator of this.operators) {
@@ -1305,9 +1279,9 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
13051279
if (this.type == InstrumentType.noise) {
13061280
getDrumWave(this.chipNoise, inverseRealFourierTransform, scaleElementsByFactor);
13071281
} else if (this.type == InstrumentType.harmonics) {
1308-
this.harmonicsWave.getCustomWave();
1282+
this.harmonicsWave.getCustomWave(this.type);
13091283
} else if (this.type == InstrumentType.pickedString) {
1310-
PickedStringImpulseWave.getWave();
1284+
this.harmonicsWave.getCustomWave(this.type);
13111285
} else if (this.type == InstrumentType.spectrum) {
13121286
this.spectrumWave.getCustomWave(8);
13131287
} else if (this.type == InstrumentType.drumset) {
@@ -1509,6 +1483,15 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
15091483
buffer.push(SongTagCode.chord, base64IntToCharCode[instrument.chord]);
15101484
}
15111485

1486+
if (instrument.type == InstrumentType.harmonics || instrument.type == InstrumentType.pickedString) {
1487+
buffer.push(SongTagCode.harmonics);
1488+
const harmonicsBits: BitFieldWriter = new BitFieldWriter();
1489+
for (let i: number = 0; i < Config.harmonicsControlPoints; i++) {
1490+
harmonicsBits.write(Config.harmonicsControlPointBits, instrument.harmonicsWave.harmonics[i]);
1491+
}
1492+
harmonicsBits.encodeBase64(buffer);
1493+
}
1494+
15121495
if (instrument.type == InstrumentType.chip) {
15131496
buffer.push(SongTagCode.wave, base64IntToCharCode[instrument.chipWave]);
15141497
buffer.push(SongTagCode.vibrato, base64IntToCharCode[instrument.vibrato]);
@@ -1558,19 +1541,11 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
15581541
} else if (instrument.type == InstrumentType.harmonics) {
15591542
buffer.push(SongTagCode.vibrato, base64IntToCharCode[instrument.vibrato]);
15601543
buffer.push(SongTagCode.interval, base64IntToCharCode[instrument.interval]);
1561-
1562-
buffer.push(SongTagCode.harmonics);
1563-
const harmonicsBits: BitFieldWriter = new BitFieldWriter();
1564-
for (let i: number = 0; i < Config.harmonicsControlPoints; i++) {
1565-
harmonicsBits.write(Config.harmonicsControlPointBits, instrument.harmonicsWave.harmonics[i]);
1566-
}
1567-
harmonicsBits.encodeBase64(buffer);
15681544
} else if (instrument.type == InstrumentType.pwm) {
15691545
buffer.push(SongTagCode.vibrato, base64IntToCharCode[instrument.vibrato]);
15701546
// TODO: The envelope should be saved separately.
15711547
buffer.push(SongTagCode.pulseWidth, base64IntToCharCode[instrument.pulseWidth], base64IntToCharCode[instrument.pulseEnvelope]);
15721548
} else if (instrument.type == InstrumentType.pickedString) {
1573-
buffer.push(SongTagCode.pulseWidth, base64IntToCharCode[instrument.pulseWidth]);
15741549
buffer.push(SongTagCode.vibrato, base64IntToCharCode[instrument.vibrato]);
15751550
buffer.push(SongTagCode.stringSustain, base64IntToCharCode[instrument.stringSustain]);
15761551
} else {
@@ -4757,7 +4732,7 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
47574732

47584733
private static harmonicsSynth(synth: Synth, bufferIndex: number, runLength: number, tone: Tone, instrument: Instrument): void {
47594734
const data: Float32Array = synth.tempMonoInstrumentSampleBuffer!;
4760-
const wave: Float32Array = instrument.harmonicsWave.getCustomWave();
4735+
const wave: Float32Array = instrument.harmonicsWave.getCustomWave(instrument.type);
47614736
const waveLength: number = wave.length - 1; // The first sample is duplicated at the end, don't double-count it.
47624737

47634738
const intervalA: number = +Math.pow(2.0, (Config.intervals[instrument.interval].offset + Config.intervals[instrument.interval].spread) / 12.0);
@@ -4932,6 +4907,12 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
49324907
// Also, if the pitch changed suddenly (e.g. from seamless or arpeggio) then reset the wave.
49334908

49344909
delayIndex = 0;
4910+
allPassSample = 0.0;
4911+
allPassPrevInput = 0.0;
4912+
shelfSample = 0.0;
4913+
shelfPrevInput = 0.0;
4914+
fractionalDelaySample = 0.0;
4915+
49354916
// Clear away a region of the delay buffer for the new impulse.
49364917
const startImpulseFrom: number = -delayLength;
49374918
const startZerosFrom: number = Math.floor(startImpulseFrom - periodLengthStart / 2);
@@ -4941,51 +4922,27 @@ const epsilon: number = (1.0e-24); // For detecting and avoiding float denormals
49414922
delayLine[i & delayBufferMask] = 0.0;
49424923
}
49434924

4944-
allPassSample = 0.0;
4945-
allPassPrevInput = 0.0;
4946-
shelfSample = 0.0;
4947-
shelfPrevInput = 0.0;
4948-
fractionalDelaySample = 0.0;
4949-
4950-
const impulseWave: Float32Array = PickedStringImpulseWave.getWave();
4951-
const impulseWaveLength: number = +impulseWave.length - 1; // The first sample is duplicated at the end, don't double-count it.
4925+
const impulseWave: Float32Array = instrument.harmonicsWave.getCustomWave(instrument.type);
4926+
const impulseWaveLength: number = impulseWave.length - 1; // The first sample is duplicated at the end, don't double-count it.
49524927
const impulsePhaseDelta: number = impulseWaveLength / periodLengthStart;
4953-
const pulseOffset: number = periodLengthStart * (tone.pulseWidth * (1.0 + (Math.random() - 0.5) * Config.pickedStringPulseWidthRandomness));
4954-
const impulseExpressionMult: number = 0.5; // Compensate for adding two copies of the wave.
4955-
4956-
const startFirstWaveFrom: number = startImpulseFrom;
4957-
const startFirstWaveFromSample: number = Math.ceil(startFirstWaveFrom);
4958-
const stopFirstWaveAtSample: number = Math.floor(startImpulseFrom + periodLengthStart);
4959-
const startFirstWavePhase: number = (startFirstWaveFromSample - startFirstWaveFrom) * impulsePhaseDelta;
4960-
const startSecondWaveFrom: number = startFirstWaveFrom + pulseOffset;
4961-
const startSecondWaveFromSample: number = Math.ceil(startSecondWaveFrom);
4962-
const stopSecondWaveAtSample: number = Math.floor(startSecondWaveFrom + periodLengthStart);
4963-
const startSecondWavePhase: number = (startSecondWaveFromSample - startSecondWaveFrom) * impulsePhaseDelta;
4964-
4965-
let impulsePhase: number = startFirstWavePhase;
4966-
let prevWaveIntegral: number = 0.0;
4967-
for (let i: number = startFirstWaveFromSample; i <= stopFirstWaveAtSample; i++) {
4968-
const impulsePhaseInt: number = impulsePhase|0;
4969-
const index: number = impulsePhaseInt % impulseWaveLength;
4970-
let nextWaveIntegral: number = impulseWave[index];
4971-
const phaseRatio: number = impulsePhase - impulsePhaseInt;
4972-
nextWaveIntegral += (impulseWave[index+1] - nextWaveIntegral) * phaseRatio;
4973-
const sample: number = (nextWaveIntegral - prevWaveIntegral) / impulsePhaseDelta;
4974-
delayLine[i & delayBufferMask] += sample * impulseExpressionMult;
4975-
prevWaveIntegral = nextWaveIntegral;
4976-
impulsePhase += impulsePhaseDelta;
4928+
if (impulseWaveLength <= Math.ceil(periodLengthStart)) {
4929+
throw new Error("Picked string delay buffer too small to contain wave, buffer: " + impulseWaveLength + ", period: " + periodLengthStart);
49774930
}
4931+
4932+
const startImpulseFromSample: number = Math.ceil(startImpulseFrom);
4933+
const stopImpulseAtSample: number = Math.floor(startImpulseFrom + periodLengthStart);
4934+
const startImpulsePhase: number = (startImpulseFromSample - startImpulseFrom) * impulsePhaseDelta;
49784935

4979-
impulsePhase = startSecondWavePhase;
4980-
prevWaveIntegral = 0.0;
4981-
for (let i: number = startSecondWaveFromSample; i <= stopSecondWaveAtSample; i++) {
4936+
let impulsePhase: number = startImpulsePhase;
4937+
let prevWaveIntegral: number = 0.0;
4938+
for (let i: number = startImpulseFromSample; i <= stopImpulseAtSample; i++) {
49824939
const impulsePhaseInt: number = impulsePhase|0;
49834940
const index: number = impulsePhaseInt % impulseWaveLength;
49844941
let nextWaveIntegral: number = impulseWave[index];
49854942
const phaseRatio: number = impulsePhase - impulsePhaseInt;
49864943
nextWaveIntegral += (impulseWave[index+1] - nextWaveIntegral) * phaseRatio;
49874944
const sample: number = (nextWaveIntegral - prevWaveIntegral) / impulsePhaseDelta;
4988-
delayLine[i & delayBufferMask] -= sample * impulseExpressionMult;
4945+
delayLine[i & delayBufferMask] += sample;
49894946
prevWaveIntegral = nextWaveIntegral;
49904947
impulsePhase += impulsePhaseDelta;
49914948
}

0 commit comments

Comments
 (0)