I have created a virtual piano that turns the home row (excluding 'g' and 'h') and the top row (excluding 'y' and 'u') of the user's keyboard in a piano's keyboard.
The home row is used for white keys and the top row is used for black keys.
The keyboard's keys bind to piano's as shown below:
a - C w - C# s - D e - D# d - E r - E# (F) f - F t - F# j - G i - G# k - A o - A# l - B p - B# (C) ; - C
To change octaves, you use 'z' to go up one octave and 'x' to down one octave.
The code
virtual_piano.asm
;===============================================================================
; Virtual Piano -- a virtual and playable piano
; By SirPython of Code Review and GitHub
;
; virtual_piano.asm
;===============================================================================
%define MIDI_CONTROL_PORT 0331h
%define MIDI_DATA_PORT 0330h
%define MIDI_UART_MODE 3Fh
%define MIDI_PIANO_INSTRUMENT 93h
start:
call setup_midi
mov ch, 60; default octave(0)
mov cl, 5; used for making sure that the user does not go too low or too high with the octaves
.loop:
call read_character
call process_input
cmp bh, 0; if bad input OR octave change goes out of range
je .loop
call get_pitch
cmp bh, 2; if shouldn't play note (was an octave switch)
je .loop
call play_note
jmp .loop
;--------------------------------------------------
; Plays a note
;
; IN: AL, CH = pitch, (octave * 12) + 60
; OUT: NONE
; ERR: NONE
; REG: AL
play_note:
add al, ch; apply the octave
out dx, al; DX will already contain MIDI_DATA_PORT from the setup_midi function
mov al, 7Fh; note duration
out dx, al
ret
;--------------------------------------------------
; Based on input, returns a pitch to be played
;
; IN: AL = key code
; OUT: AL, BH, CH = pitch, 2 if no pitch to be played, (octave * 12) + 60
; ERR: NONE
; REG: preserved
get_pitch:
cmp al, 'a'
je .a
cmp al, 's'
je .s
cmp al, 'd'
je .d
cmp al, 'f'
je .f
cmp al, 'j'
je .j
cmp al, 'k'
je .k
cmp al, 'l'
je .l
cmp al, ';'
je .sc
cmp al, 'w'
je .w
cmp al, 'e'
je .e
cmp al, 'r'
je .r
cmp al, 't'
je .t
cmp al, 'i'
je .i
cmp al, 'o'
je .o
cmp al, 'p'
je .p
cmp al, 'z'
je .z
cmp al, 'x'
je .x
.a: mov al, 0
jmp .end
.s: mov al, 2
jmp .end
.d: mov al, 4
jmp .end
.f: mov al, 5
jmp .end
.j: mov al, 7
jmp .end
.k: mov al, 9
jmp .end
.l: mov al, 11
jmp .end
.sc: mov al, 12
jmp .end
.w: mov al, 1
jmp .end
.e: mov al, 3
jmp .end
.r: jmp .f
jmp .end
.t: mov al, 6
jmp .end
.i: mov al, 8
jmp .end
.o: mov al, 10
jmp .end
.p: jmp .l
jmp .end
.z: add ch, 12
add cl, 1
mov bh, 2
jmp .end
.x: sub ch, 12
sub cl, 1
mov bh, 2
jmp .end
.end:
ret
;--------------------------------------------------
; Set's up the MIDI ports for use
;
; IN: NONE
; OUT: NONE
; ERR: NONE
; REG: DX
setup_midi:
push ax
mov dx, MIDI_CONTROL_PORT
mov al, MIDI_UART_MODE; play notes as soon as they are recieved
out dx, al
mov dx, MIDI_DATA_PORT
mov al, MIDI_PIANO_INSTRUMENT
out dx, al
pop ax
ret
;--------------------------------------------------
; Checks to make sure that input is acceptable
;
; IN: AL = key code
; OUT: BH = 1 (accpetable) or 0 (not acceptable, or octave is trying to change too far)
; ERR: NONE
; REG: preserved
process_input:
.check_key_code:
cmp al, 'a'
je .safe
cmp al, 's'
je .safe
cmp al, 'd'
je .safe
cmp al, 'f'
je .safe
cmp al, 'j'
je .safe
cmp al, 'k'
je .safe
cmp al, 'l'
je .safe
cmp al, ';'
je .safe
cmp al, 'w'
je .safe
cmp al, 'e'
je .safe
cmp al, 'r'
je .safe
cmp al, 't'
je .safe
cmp al, 'i'
je .safe
cmp al, 'o'
je .safe
cmp al, 'p'
je .safe
.check_octave_code:
cmp al, 'z'
je .z
cmp al, 'x'
je .x
jmp .err; none of the keys pressed were valid keys
.z:
cmp cl, 10; if user is about to go out of octave range, then drop down to error
jne .safe
.x:
cmp cl, 1
jne .safe
.err:
xor bh, bh
ret
.safe:
mov bh, 1
ret
;--------------------------------------------------
; Reads a single character from the user
;
; IN: NONE
; OUT: AL = key code
; ERR: NONE
; REG: preserved
read_character:
xor ah, ah
int 16h; BIOS 16h 00h
ret
Build
This was written in x86 NASM Assembler and run in the DOSBOX.
You can use the Makefile below for building and running the code.
NAME=virtual_piano
ASM=nasm
ASMFLAGS=-o $(NAME).com
DIR=$(shell pwd)
DB=dosbox
DBFLAGS=-c "mount c $(DIR)" -c "C:" -c "VIRTUA~2.COM"
all:
$(ASM) $(NAME).asm $(ASMFLAGS)
run:
$(DB) $(DBFLAGS)
clean:
rm *.com
Documentation syntax
For each subroutine in the code, I created a short documentation following this syntax:
;-------------------------------------------------- ; DESCRIPTION ; ; IN: reg1, reg2, reg3 = desc1, desc2, desc3 ; OUT: reg1, reg2, reg3 = desc1, desc2, desc3 ; ERR: any errors that could possibly be produced and how the output will change if they are produced ; REG: any registers that will not be preserved (excluding ones holding the returns). Or, if all are being preserved (excluding the ones holding the returns), just "preserved"
Concerns:
Those 'switch statements' in the subroutines
get_pitch
andprocess_input
. It is very, very ugly. And, with all the conditional checks, I'm assuming it's also very, very inefficient. Is there a better way I could do this?My register use for input and output of the subroutines. I know that it doesn't really matter whether or not I'm using
AX
orBX
in this specific code but, for example, is it okay that I stuff a return code inBH
? Or that I useCH
to hold octave values, rather than another register?Are the subroutines doing their job and only their job? I'm especially concerned about
get_pitch
andprocess_input
; they both do the same kind of thing, but they both return completely separate values. Is one doing another's job? Or, how about combining them. Would that be a good idea?Is my code understandable? Is it easy to follow what is happening and why it is happening?
How is my documentation? I know many of you hate having lines going across the screen like that. If you are on of those people, how else do you recommend doing it? Or, if you aren't one of those people, is there anything differently I should do with the documentation.
All that MIDI port stuff and instrument stuff came from various documentations unofficial documentations and codes that I read from the internet. These numbers came pieced from those. Should I, for the goal of my code, be using different values/ports for MIDI/sound?
Any other recommendations are encouraged.