diff --git a/pxtblocks/fields/field_note.ts b/pxtblocks/fields/field_note.ts index 3ee5745fbe19..22034b128da6 100644 --- a/pxtblocks/fields/field_note.ts +++ b/pxtblocks/fields/field_note.ts @@ -249,6 +249,11 @@ export class FieldNote extends Blockly.FieldNumber implements FieldCustom { this.refreshText() }; + protected widgetDispose_(): void { + this.htmlInput_.removeEventListener("keydown", this.keyHandler); + super.widgetDispose_(); + } + /** * Create a piano under the note field. */ @@ -330,6 +335,24 @@ export class FieldNote extends Blockly.FieldNumber implements FieldCustom { Blockly.DropDownDiv.setColour(this.primaryColour, this.borderColour); Blockly.DropDownDiv.showPositionedByBlock(this, this.sourceBlock_ as Blockly.BlockSvg, () => this.onHide()); + + + this.htmlInput_.addEventListener("keydown", this.keyHandler); + } + + protected keyHandler = (ev: KeyboardEvent) => { + const currentFreq = typeof this.value_ === "string" ? parseFloat(this.value_) : this.value_; + + if (ev.code === "ArrowUp" || ev.code === "ArrowDown") { + const {keyAbove, keyBelow} = this.getNeighboringKeys(currentFreq); + const newKey = ev.code === "ArrowUp" ? keyAbove : keyBelow; + const newFrequency = this.getKeyFreq(newKey); + this.setValue(newFrequency); + this.playKey(this.piano[newKey - this.minNote_], newFrequency); + this.noteLabel.textContent = this.getKeyName(newKey); + ev.stopPropagation(); + ev.preventDefault(); + } } protected playKey(key: HTMLDivElement, frequency: number) { @@ -525,6 +548,42 @@ export class FieldNote extends Blockly.FieldNumber implements FieldCustom { return FieldNote.keyHeight / 2; } + protected getNeighboringKeys(freq: number) { + let lowerBound; + let upperBound; + + for (let candidate = this.minNote_; candidate <= this.maxNote_; ++candidate) { + if (this.getKeyFreq(candidate) + this.eps > freq) { + upperBound = candidate; + break; + } + lowerBound = candidate; + } + + // Clamp key range at the top and bottom + if (!lowerBound) { + return { + keyAbove: freq + this.eps > this.getKeyFreq(this.minNote_) ? this.minNote_ + 1 : this.minNote_, + keyBelow: this.minNote_ + }; + } + if (!upperBound || upperBound === this.maxNote_) { + return { + keyAbove: this.maxNote_, + keyBelow: freq - this.eps < this.getKeyFreq(this.maxNote_) ? this.maxNote_ - 1 : this.maxNote_ + }; + } + + // If we are within an epsilon of confidence, only consider one boundary point + if (freq < this.getKeyFreq(lowerBound) + this.eps) { + upperBound = lowerBound; + } else if (freq > this.getKeyFreq(upperBound) - this.eps) { + lowerBound = upperBound; + } + + return {keyAbove: lowerBound + 1, keyBelow: upperBound - 1}; + } + protected getKeyFreq(keyIndex: number) { return this.getKeyNoteData(keyIndex).freq; }