Skip to content

Commit 6b02537

Browse files
committed
Added a slightly different sustain style option.
1 parent afc81d1 commit 6b02537

File tree

8 files changed

+274
-79
lines changed

8 files changed

+274
-79
lines changed

editor/EditorConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function prettyNumber(value: number): string {
2222
}
2323

2424
export class EditorConfig {
25-
public static readonly version: string = "4.1";
25+
public static readonly version: string = "4.1.1";
2626

2727
public static readonly versionDisplayName: string = "BeepBox";
2828
public static readonly releaseNotesURL: string = "https://github.com/johnnesky/beepbox/releases/tag/v" + EditorConfig.version;

editor/SongEditor.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {Piano} from "./Piano";
2828
import {BeatsPerBarPrompt} from "./BeatsPerBarPrompt";
2929
import {MoveNotesSidewaysPrompt} from "./MoveNotesSidewaysPrompt";
3030
import {SongDurationPrompt} from "./SongDurationPrompt";
31+
import {SustainPrompt} from "./SustainPrompt";
3132
import {ChannelSettingsPrompt} from "./ChannelSettingsPrompt";
3233
import {ExportPrompt} from "./ExportPrompt";
3334
import {ImportPrompt} from "./ImportPrompt";
@@ -249,7 +250,8 @@ export class SongEditor {
249250
private readonly _bitcrusherFreqSlider: Slider = new Slider(input({style: "margin: 0;", type: "range", min: "0", max: Config.bitcrusherFreqRange - 1, value: "0", step: "1"}), this._doc, (oldValue: number, newValue: number) => new ChangeBitcrusherFreq(this._doc, oldValue, newValue));
250251
private readonly _bitcrusherFreqRow: HTMLDivElement = div({class: "selectRow"}, span({class: "tip", onclick: ()=>this._openPrompt("bitcrusherFreq")}, "Freq Crush:"), this._bitcrusherFreqSlider.input);
251252
private readonly _stringSustainSlider: Slider = new Slider(input({style: "margin: 0;", type: "range", min: "0", max: Config.stringSustainRange - 1, value: "0", step: "1"}), this._doc, (oldValue: number, newValue: number) => new ChangeStringSustain(this._doc, oldValue, newValue));
252-
private readonly _stringSustainRow: HTMLDivElement = div({class: "selectRow"}, span({class: "tip", onclick: ()=>this._openPrompt("stringSustain")}, "Sustain:"), this._stringSustainSlider.input);
253+
private readonly _stringSustainLabel: HTMLSpanElement = span({class: "tip", onclick: ()=>this._openPrompt("stringSustain")}, "Sustain:");
254+
private readonly _stringSustainRow: HTMLDivElement = div({class: "selectRow"}, this._stringSustainLabel, this._stringSustainSlider.input);
253255
private readonly _unisonSelect: HTMLSelectElement = buildOptions(select(), Config.unisons.map(unison=>unison.name));
254256
private readonly _unisonSelectRow: HTMLElement = div({class: "selectRow"}, span({class: "tip", onclick: ()=>this._openPrompt("unison")}, "Unison:"), div({class: "selectContainer"}, this._unisonSelect));
255257
private readonly _chordSelect: HTMLSelectElement = buildOptions(select(), Config.chords.map(chord=>chord.name));
@@ -605,7 +607,7 @@ export class SongEditor {
605607
this._currentPromptName = promptName;
606608

607609
if (this.prompt) {
608-
if (this._wasPlaying && !(this.prompt instanceof TipPrompt)) {
610+
if (this._wasPlaying && !(this.prompt instanceof TipPrompt || this.prompt instanceof SustainPrompt)) {
609611
this._doc.performance.play();
610612
}
611613
this._wasPlaying = false;
@@ -645,13 +647,16 @@ export class SongEditor {
645647
case "recordingSetup":
646648
this.prompt = new RecordingSetupPrompt(this._doc);
647649
break;
650+
case "stringSustain":
651+
this.prompt = new SustainPrompt(this._doc);
652+
break;
648653
default:
649654
this.prompt = new TipPrompt(this._doc, promptName);
650655
break;
651656
}
652657

653658
if (this.prompt) {
654-
if (!(this.prompt instanceof TipPrompt)) {
659+
if (!(this.prompt instanceof TipPrompt || this.prompt instanceof SustainPrompt)) {
655660
this._wasPlaying = this._doc.synth.playing;
656661
this._doc.performance.pause();
657662
}
@@ -812,6 +817,7 @@ export class SongEditor {
812817
if (instrument.type == InstrumentType.pickedString) {
813818
this._stringSustainRow.style.display = "";
814819
this._stringSustainSlider.updateValue(instrument.stringSustain);
820+
this._stringSustainLabel.textContent = "Sustain (" + Config.sustainTypeNames[instrument.stringSustainType].substring(0,1).toUpperCase() + "):";
815821
} else {
816822
this._stringSustainRow.style.display = "none";
817823
}

editor/SustainPrompt.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) 2012-2022 John Nesky and contributing authors, distributed under the MIT license, see accompanying the LICENSE.md file.
2+
3+
import {Config} from "../synth/SynthConfig";
4+
import {Instrument} from "../synth/synth";
5+
import {HTML} from "imperative-html/dist/esm/elements-strict";
6+
import {SongDocument} from "./SongDocument";
7+
import {Prompt} from "./Prompt";
8+
import {ChangeGroup} from "./Change";
9+
import {ChangeStringSustainType} from "./changes";
10+
11+
const {button, div, label, h2, p, select, option} = HTML;
12+
13+
export class SustainPrompt implements Prompt {
14+
private readonly _typeSelect: HTMLSelectElement = select({style: "width: 100%;"},
15+
option({value: "acoustic"}, "(A) Acoustic"),
16+
option({value: "bright"}, "(B) Bright"),
17+
);
18+
private readonly _cancelButton: HTMLButtonElement = button({class: "cancelButton"});
19+
private readonly _okayButton: HTMLButtonElement = button({class: "okayButton", style: "width:45%;"}, "Okay");
20+
21+
public readonly container: HTMLDivElement = div({class: "prompt", style: "width: 300px;"},
22+
div(
23+
h2("String Sustain"),
24+
p("This setting controls how quickly the picked string vibration decays."),
25+
p("Unlike most of BeepBox's instrument synthesizer features, a picked string cannot change frequency suddenly while maintaining its decay. If a tone's pitch changes suddenly (e.g. if the chord type is set to \"arpeggio\" or the transition type is set to \"continues\") then the string will be re-picked and start decaying from the beginning again, even if the envelopes don't otherwise restart."),
26+
),
27+
div(
28+
p("BeepBox comes with two slightly different sustain designs. You can select one here and press \"Okay\" to confirm it."),
29+
div({class: "selectContainer", style: "width: 100%;"}, this._typeSelect),
30+
),
31+
label({style: "display: flex; flex-direction: row-reverse; justify-content: space-between;"},
32+
this._okayButton,
33+
),
34+
this._cancelButton,
35+
);
36+
37+
constructor(private _doc: SongDocument) {
38+
const instrument: Instrument = this._doc.song.channels[this._doc.channel].instruments[this._doc.getCurrentInstrument()];
39+
this._typeSelect.value = Config.sustainTypeNames[instrument.stringSustainType];
40+
41+
setTimeout(()=>this._cancelButton.focus());
42+
43+
this._okayButton.addEventListener("click", this._saveChanges);
44+
this._cancelButton.addEventListener("click", this._close);
45+
this.container.addEventListener("keydown", this._whenKeyPressed);
46+
}
47+
48+
private _close = (): void => {
49+
this._doc.undo();
50+
}
51+
52+
public cleanUp = (): void => {
53+
this._okayButton.removeEventListener("click", this._saveChanges);
54+
this._cancelButton.removeEventListener("click", this._close);
55+
this.container.removeEventListener("keydown", this._whenKeyPressed);
56+
}
57+
58+
private _whenKeyPressed = (event: KeyboardEvent): void => {
59+
if ((<Element> event.target).tagName != "BUTTON" && event.keyCode == 13) { // Enter key
60+
this._saveChanges();
61+
}
62+
}
63+
64+
private _saveChanges = (): void => {
65+
const group: ChangeGroup = new ChangeGroup();
66+
group.append(new ChangeStringSustainType(this._doc, <any> Config.sustainTypeNames.indexOf(this._typeSelect.value)));
67+
this._doc.prompt = null;
68+
this._doc.record(group, true);
69+
}
70+
}

editor/TipPrompt.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -272,13 +272,6 @@ export class TipPrompt implements Prompt {
272272
p("Every other notch on this slider is aligned with the currently selected key of the song, and the in-between notches are aligned with the tritones of the key."),
273273
);
274274
} break;
275-
case "stringSustain": {
276-
message = div(
277-
h2("String sustain"),
278-
p("This setting controls how quickly the picked string vibration decays."),
279-
p("Unlike most of BeepBox's instrument synthesizer features, a picked string cannot change frequency suddenly while maintaining its decay. If a tone's pitch changes suddenly (e.g. if the chord type is set to \"arpeggio\" or the transition type is set to \"continues\") then the string will be re-picked and start decaying from the beginning again, even if the envelopes don't otherwise restart."),
280-
);
281-
} break;
282275
case "envelopes": {
283276
message = div(
284277
h2("Envelopes"),

editor/changes.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright (c) 2012-2022 John Nesky and contributing authors, distributed under the MIT license, see accompanying the LICENSE.md file.
22

3-
import {Algorithm, Dictionary, FilterType, InstrumentType, EffectType, AutomationTarget, Config, effectsIncludePanning, effectsIncludeDistortion} from "../synth/SynthConfig";
3+
import {Algorithm, Dictionary, FilterType, SustainType, InstrumentType, EffectType, AutomationTarget, Config, effectsIncludePanning, effectsIncludeDistortion} from "../synth/SynthConfig";
44
import {NotePin, Note, makeNotePin, Pattern, FilterSettings, FilterControlPoint, SpectrumWave, HarmonicsWave, Instrument, Channel, Song, Synth} from "../synth/synth";
55
import {Preset, PresetCategory, EditorConfig} from "./EditorConfig";
66
import {Change, ChangeGroup, ChangeSequence, UndoableChange} from "./Change";
@@ -1367,6 +1367,20 @@ export class ChangeStringSustain extends ChangeInstrumentSlider {
13671367
}
13681368
}
13691369

1370+
export class ChangeStringSustainType extends Change {
1371+
constructor(doc: SongDocument, newValue: SustainType) {
1372+
super();
1373+
const instrument: Instrument = doc.song.channels[doc.channel].instruments[doc.getCurrentInstrument()];
1374+
const oldValue: SustainType = instrument.stringSustainType;
1375+
if (oldValue != newValue) {
1376+
instrument.stringSustainType = newValue;
1377+
instrument.preset = instrument.type;
1378+
doc.notifier.changed();
1379+
this._didSomething();
1380+
}
1381+
}
1382+
}
1383+
13701384
export class ChangeFilterAddPoint extends UndoableChange {
13711385
private _doc: SongDocument;
13721386
private _instrument: Instrument;

synth/SynthConfig.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export const enum FilterType {
3535
length,
3636
}
3737

38+
export const enum SustainType {
39+
bright,
40+
acoustic,
41+
length,
42+
}
43+
3844
export const enum EnvelopeType {
3945
noteSize,
4046
none,
@@ -481,10 +487,11 @@ export class Config {
481487
public static readonly pickedStringDispersionFreqScale: number = 0.3; // The tone fundamental freq freq moves this much toward the center freq for computing the all-pass corner freq.
482488
public static readonly pickedStringDispersionFreqMult: number = 4.0; // The all-pass corner freq is based on this times the adjusted tone fundamental freq.
483489
public static readonly pickedStringShelfHz: number = 4000.0; // The cutoff freq of the shelf filter that is used to decay the high frequency energy in the picked string.
484-
485-
public static readonly distortionRange: number = 8;
486490
public static readonly stringSustainRange: number = 15;
487491
public static readonly stringDecayRate: number = 0.12;
492+
public static readonly sustainTypeNames: ReadonlyArray<string> = ["bright", "acoustic"]; // See SustainType enum above.
493+
494+
public static readonly distortionRange: number = 8;
488495
public static readonly bitcrusherFreqRange: number = 14;
489496
public static readonly bitcrusherOctaveStep: number = 0.5;
490497
public static readonly bitcrusherQuantizationRange: number = 8;

synth/filtering.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ denominator with the imaginary component negated.)
167167
Finally, I'll list some of the links that helped me understand filters and
168168
provided some of the algorithms I that use here.
169169
170-
Here's where I found accurate 2nd order low-pass and high-pass digital filters:
170+
Here's where I found accurate 2nd order low-pass, high-pass, and high-shelf
171+
digital filters:
171172
https://web.archive.org/web/20120531011328/http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt
172173
173174
This page is how I found a link to the cookbook article above. It claims these
@@ -366,6 +367,23 @@ export class FilterCoefficients {
366367
this.order = 2;
367368
}
368369
*/
370+
371+
public highShelf2ndOrder(cornerRadiansPerSample: number, shelfLinearGain: number, slope: number): void {
372+
const A: number = Math.sqrt(shelfLinearGain);
373+
const c: number = Math.cos(cornerRadiansPerSample);
374+
const Aplus: number = A + 1.0;
375+
const Aminus: number = A - 1.0;
376+
const alpha: number = Math.sin(cornerRadiansPerSample) * 0.5 * Math.sqrt((Aplus / A) * (1.0 / slope - 1.0) + 2.0);
377+
const sqrtA2Alpha: number = 2.0 * Math.sqrt(A) * alpha;
378+
const a0: number = (Aplus - Aminus * c + sqrtA2Alpha);
379+
this.a[1] = 2 * (Aminus - Aplus * c ) / a0;
380+
this.a[2] = (Aplus - Aminus * c - sqrtA2Alpha) / a0;
381+
this.b[0] = A * (Aplus + Aminus * c + sqrtA2Alpha) / a0;
382+
this.b[1] = -2 * A * (Aminus + Aplus * c ) / a0;
383+
this.b[2] = A * (Aplus + Aminus * c - sqrtA2Alpha) / a0;
384+
this.order = 2;
385+
}
386+
369387
public peak2ndOrder(cornerRadiansPerSample: number, peakLinearGain: number, bandWidthScale: number): void {
370388
const sqrtGain: number = Math.sqrt(peakLinearGain);
371389
const bandWidth: number = bandWidthScale * cornerRadiansPerSample / (sqrtGain >= 1 ? sqrtGain : 1/sqrtGain);
@@ -503,3 +521,17 @@ export class DynamicBiquadFilter {
503521
this.useMultiplicativeInputCoefficients = useMultiplicativeInputCoefficients;
504522
}
505523
}
524+
525+
// Filters are typically designed as analog filters first, then converted to
526+
// digital filters using one of two methods: the "matched z-transform" or the
527+
// "bilinear transform". The "bilinear transform" does a better job of
528+
// preserving the magnitudes of the frequency response, but warps the frequency
529+
// range such that the nyquist frequency of the digital filter (π) maps to the
530+
// infinity frequency of the analog filter. You can use the below functions to
531+
// manually perform this warping in either direction.
532+
export function warpNyquistToInfinity(radians: number): number {
533+
return 2.0 * Math.tan(radians * 0.5);
534+
}
535+
export function warpInfinityToNyquist(radians: number): number {
536+
return 2.0 * Math.atan(radians * 0.5);
537+
}

0 commit comments

Comments
 (0)