So with both the doubly linked list and a singly linked list (both of which are basically just a FIFO (queue) instead of a FILO (stack)) you still effectively have a gap problem.
Press A, Press B, Press C, release B....
You now have a gap in the middle of the list, so your tail pointer in a FIFO would have the same issue, and ultimately someone COULD still blow your array if they're clever with how they press the keys. A doubly linked list could help in that you can adjust the head/tail pointers in the list, but you'd then still have the O(n) problem where n is the distance from head to tail. Which n is arguably small in this case, but we're talking about a 6502, so those 1s add up.
Again, if you're actually playing single notes, this isn't something I foresee as being something that would generally happen. If someone presses a chord it might start getting a bit crazy though, as the software will almost certainly get the keys released in a completely different order than they were pressed. Ultimately the order issue would likely get resolved on subsequent passes of the scan loop though.
In any event I don't see a FIFO as having any additional benefit over a FILO, in fact it adds the complexity of having an additional pointer to deal with; plus you have to implement this all in software. At least the stack is mostly done in the hardware.
Coding for a modular synthesizer keyboard help
-
jimmydmusic
- Posts: 9
- Joined: 09 Sep 2025
Re: Coding for a modular synthesizer keyboard help
The assembly code that I wrote that works fine but it's one note at a time, meaning if you hold down a key and press another while still holding the first key down is hangs on to the first key until you release it. It's pretty good but when trying to play a fast arpeggio it's definitely noticeable the lag in time.
I've been an accomplished keyboardist since I was 7 taking classical piano lessons so I can play pretty fast and it's frustrating to play a slow keyboard. Anyway I hope these files help.
Thanks again everyone for lending a helping hand for an ole long haired musician.
I've been an accomplished keyboardist since I was 7 taking classical piano lessons so I can play pretty fast and it's frustrating to play a slow keyboard. Anyway I hope these files help.
Thanks again everyone for lending a helping hand for an ole long haired musician.
Last edited by jimmydmusic on Mon Sep 22, 2025 12:33 pm, edited 1 time in total.
Re: Coding for a modular synthesizer keyboard help
I know the feeling, I've played on a keyboard like that before - unless you play staccato it can really mess up your timing.
Assuming your system has plenty of memory (at least, 80-odd bytes of it to spare, really not much in the scheme of things) there's no need for anything very fancy - I'd just store a flag per key saying whether it's currently known to be down, and keep that array up-to-date with your scan. Any time your scan spots a key that's in a different up/down state to what was in the array, issue a press or release event.
Regarding what to do with the events - given that the synth only has one channel, I think it's fairly simple:
Assuming your system has plenty of memory (at least, 80-odd bytes of it to spare, really not much in the scheme of things) there's no need for anything very fancy - I'd just store a flag per key saying whether it's currently known to be down, and keep that array up-to-date with your scan. Any time your scan spots a key that's in a different up/down state to what was in the array, issue a press or release event.
Regarding what to do with the events - given that the synth only has one channel, I think it's fairly simple:
- On a release event, if it corresponds to the note that is currently playing, end the note. Otherwise do nothing.
- On a press event, if a note is currently playing, end it; and regardless, start the new note.
Re: Coding for a modular synthesizer keyboard help
Yuri wrote:
So with both the doubly linked list and a singly linked list (both of which are basically just a FIFO (queue) instead of a FILO (stack)) you still effectively have a gap problem.
Press A, Press B, Press C, release B....
You now have a gap in the middle of the list
Press A, Press B, Press C, release B....
You now have a gap in the middle of the list
Here's some code to add a key (in the X register) to the list. head and tail are the indices of the list's head and tail items. next and prev are arrays of 76+2 bytes each.
Code: Select all
lda next+head
tay ; we'll need this again later
sta next, x ; key.next = old head.next
lda #head
sta prev, x ; key.prev = head
txa
sta prev, y ; head.next.prev = key
sta next+head ; head.next = key
Code: Select all
lda next, x
tay
lda prev, x
sta prev, y ; key.next.prev = old key.prev
tax
tya
sta next, x ; key.prev.next = old key.next
Code: Select all
Initial state:
next $ff $ff $ff $ff $03
prev $ff $ff $ff $04 $ff
(Head <-> Tail)
Press A:
next $03 $ff $ff $ff $00
prev $04 $ff $ff $00 $ff
(Head <-> A <-> Tail)
Press B:
next $03 $00 $ff $ff $01
prev $01 $04 $ff $00 $ff
(Head <-> B <-> A <-> Tail)
Press C:
next $03 $00 $01 $ff $02
prev $01 $02 $04 $00 $ff
(Head <-> C <-> B <-> A <-> Tail)
Release B:
next $03 $00 $00 $ff $02
prev $02 $02 $04 $00 $ff
(Head <-> C <-> A <-> Tail)
There are no gaps in the list - that's the point of using a list. The code for both adding and removing items is O(1) because we always know where to find them. No searching is required, which is usually the case with linked lists.
I don't have a 6502 set up for testing right now, so here's my 65020 test code and the output it gives. At initialisation and after each operation it shows the contents of the arrays (a reduced version with only three keys), then walks the list from head to tail and tail to head.
Code: Select all
.section "vars"
numKeys = 3
tail = numKeys
head = numKeys+1
next .space numKeys+2
prev .space numKeys+2
.section "code"
bra.l init
bra.l report
ldr x0, #0
bra.l insert
bra.l report
ldr x0, #1
bra.l insert
bra.l report
ldr x0, #2
bra.l insert
bra.l report
ldr x0, #1
bra.l remove
bra.l report
bra *
init
{
ldr a0, #$ff
ldr x0, #numKeys+2
loop
str a0, next, x0
str a0, prev, x0
dec x0
bpl loop
sti next+head, #tail
sti prev+tail, #head
rts
}
insert
{
ldr a0, next+head
mov y0, a0 ; we'll need this again later
str a0, next, x0 ; key.next = old head.next
ldr a0, #head
str a0, prev, x0 ; key.prev = head
mov a0, x0
str a0, prev, y0 ; head.next.prev = key
str a0, next+head ; head.next = key
rts
}
remove
{
ldr a0, next, x0
mov y0, a0
ldr a0, prev, x0
str a0, prev, y0 ; key.next.prev = old key.prev
mov x0, a0
mov a0, y0
str a0, next, x0 ; key.prev.next = old key.next
rts
}
report
{
brk 3, #syscall_print_inline_string
.byte "next: ", 0
ldr.l y0, #next
bra.l printArray
brk 3, #syscall_print_inline_string
.byte "prev: ", 0
ldr.l y0, #prev
bra.l printArray
ldr x0, #head
forwardLoop
brk 3, #syscall_print_inline_string
.byte char_reg_hex2+char_reg_x0, " ", 0
ldr x0, next, x0
cmp x0, #$ff
bne forwardLoop
brk 3, #syscall_print_inline_string
.byte 13, 0
ldr x0, #tail
backLoop
brk 3, #syscall_print_inline_string
.byte char_reg_hex2+char_reg_x0, " ", 0
ldr x0, prev, x0
cmp x0, #$ff
bne backLoop
brk 3, #syscall_print_inline_string
.byte 13, 0
rts
}
printArray
{
ldr x0, #numKeys+2
loop
ldr a0, 0, y0
brk 3, #syscall_print_inline_string
.byte char_reg_hex2+char_reg_a0, " ", 0
inc.l y0
dec x0
bne loop
brk 3, #syscall_print_inline_string
.byte 13, 0
rts
}
next: ff ff ff ff ff 03
prev: ff ff ff ff 04 ff
04 03
03 04
next: 03 ff ff ff 00
prev: 04 ff ff 00 ff
04 00 03
03 00 04
next: 03 00 ff ff 01
prev: 01 04 ff 00 ff
04 01 00 03
03 00 01 04
next: 03 00 01 ff 02
prev: 01 02 04 00 ff
04 02 01 00 03
03 00 01 02 04
next: 03 00 00 ff 02
prev: 02 02 04 00 ff
04 02 00 03
03 00 02 04
Re: Coding for a modular synthesizer keyboard help
If you support a singly linked list of, say, 12-16 notes then you can support as many keys as can be reasonably held down with 2 hands. When a key is released you scan the list and remove it. If you remove it from the head, then the new head becomes the new note to play.
If you want to support the sustain pedal then one way is to use a 72-entry doubly linked list along with a direct note-indexed 72-entry array that gives the linked list node index for each held note. When a key is released, it's an O(1) operation to remove it. N-note polyphony then means playing the first N notes in the list.
Having notes start playing when you release a key feels like it's going to sound very wrong when you hear it. It seems like you have to at least keep track of the duration of each held note, even if it's not actually making sound, so that you can bring it into the chord at the right spot when sound resources become available. Even then, I'm not sure it's going to sound pleasant.
If all you're trying to do is support two notes pressed at the same time, in order to achieve a smooth monophonic flow, then just a couple of variables will suffice, and no need for lists and such.
If you want to support the sustain pedal then one way is to use a 72-entry doubly linked list along with a direct note-indexed 72-entry array that gives the linked list node index for each held note. When a key is released, it's an O(1) operation to remove it. N-note polyphony then means playing the first N notes in the list.
Having notes start playing when you release a key feels like it's going to sound very wrong when you hear it. It seems like you have to at least keep track of the duration of each held note, even if it's not actually making sound, so that you can bring it into the chord at the right spot when sound resources become available. Even then, I'm not sure it's going to sound pleasant.
If all you're trying to do is support two notes pressed at the same time, in order to achieve a smooth monophonic flow, then just a couple of variables will suffice, and no need for lists and such.
Re: Coding for a modular synthesizer keyboard help
I saw you posted the actual code you're using, which is really helpful as it shows how the hardware looks from the software point of view. See below the algorithm I was suggesting, based on how your hardware seems to work.
It's split into three portions - first, it scans all the keys looking for ones that have changed; then it rescans them as a form of debouncing, as you seem to have been doing something similar already; any keys which have still changed on this second pass then cause calls to KEYUP and KEYDOWN.
Secondly, KEYUP and KEYDOWN handle decisions about when to send events to the synth - they implement the single channel logic, so new keys terminate old ones straight away. These routines feed through to NOTEON and NOTEOFF as appropriate.
Finally NOTEON and NOTEOFF send the right events to the synth. I note that the API here does allow the synth to make its own decisions about how to handle the single channel output - it is possible that you should just make KEYUP always call NOTEON and make KEYDOWN always call NOTEOFF and let the synth itself decide when to start and end notes.
This whole routine would then need to be called on a regular basis, e.g. from a timer interrupt if you have one, or by just continually looping over "JSR SCAN".
It's split into three portions - first, it scans all the keys looking for ones that have changed; then it rescans them as a form of debouncing, as you seem to have been doing something similar already; any keys which have still changed on this second pass then cause calls to KEYUP and KEYDOWN.
Secondly, KEYUP and KEYDOWN handle decisions about when to send events to the synth - they implement the single channel logic, so new keys terminate old ones straight away. These routines feed through to NOTEON and NOTEOFF as appropriate.
Finally NOTEON and NOTEOFF send the right events to the synth. I note that the API here does allow the synth to make its own decisions about how to handle the single channel output - it is possible that you should just make KEYUP always call NOTEON and make KEYDOWN always call NOTEOFF and let the synth itself decide when to start and end notes.
This whole routine would then need to be called on a regular basis, e.g. from a timer interrupt if you have one, or by just continually looping over "JSR SCAN".
Code: Select all
.space CURNOTE 1 ; The index of the currently-playing key, or negative if none
.space KEYSTATES 76 ; Array showing last known states of all keys
.space KEYCHANGED 76 ; Array showing whether each key has changed state this scan
SCAN: LDY #75
SCANLOOP: LDA KYBD, Y ; Read current state of key
EOR KEYSTATES, Y ; Compare against last known state
STA KEYCHANGED, Y ; Store whether it is different in separate array
DEY
BPL SCANLOOP
; An extra delay loop could be inserted here for more debouncing
LDY #75
RESCANLOOP: LDA KEYCHANGED, Y ; See if we thought this key changed state
BNE CHANGED
NOCHANGE: DEY ; No state change, go to next key
BPL RESCANLOOP
RTS ; Scan complete
CHANGED: LDA KYBD, Y ; Re-check the key, as a form of debouncing
EOR KEYSTATES, Y
BEQ NOCHANGE ; Now it hasn't changed state, possibly it was a bouncing issue, ignore it
EOR KEYSTATES, Y ; Get back to the state of the key that was read at CHANGED
STA KEYSTATES, Y ; Update KEYSTATES entry
BEQ RELEASE ; If it was a release event, branch
JSR KEYDOWN ; Handle new pressed key
BRA NOCHANGE ; Return to the RESCAN loop
RELEASE: JSR KEYUP ; Handle released key
BRA NOCHANGE ; Return to the RESCAN loop
KEYUP: CPY CURNOTE ; Is this key the currently-playing note?
BNE DIFFNOTE ; If not, ignore the event
JSR NOTEOFF ; Tell synth to end the currently-playing note
LDA #$FF
STA CURNOTE ; Mark that no note is currently playing
DIFFNOTE: RTS
KEYDOWN: LDA CURNOTE ; Check if a note is playing
BMI NONOTE
JSR NOTEOFF ; Tell synth to end the currently-playing note
NONOTE: STY CURNOTE ; Remember the new note
JSR NOTEON ; Tell synth to start the note
RTS
NOTEON: TYA
ORA #$80
STA REGEN ; Turn gate on with key ID and D7=1
RTS
NOTEOFF: LDA CURNOTE
STA REGEN ; Turn gate off with key ID and D7=0
RTS
-
jimmydmusic
- Posts: 9
- Joined: 09 Sep 2025
Re: Coding for a modular synthesizer keyboard help
Thanks gfoot! I like your idea and I'll give it a try.
gfoot wrote:
I saw you posted the actual code you're using, which is really helpful as it shows how the hardware looks from the software point of view. See below the algorithm I was suggesting, based on how your hardware seems to work.
It's split into three portions - first, it scans all the keys looking for ones that have changed; then it rescans them as a form of debouncing, as you seem to have been doing something similar already; any keys which have still changed on this second pass then cause calls to KEYUP and KEYDOWN.
Secondly, KEYUP and KEYDOWN handle decisions about when to send events to the synth - they implement the single channel logic, so new keys terminate old ones straight away. These routines feed through to NOTEON and NOTEOFF as appropriate.
Finally NOTEON and NOTEOFF send the right events to the synth. I note that the API here does allow the synth to make its own decisions about how to handle the single channel output - it is possible that you should just make KEYUP always call NOTEON and make KEYDOWN always call NOTEOFF and let the synth itself decide when to start and end notes.
This whole routine would then need to be called on a regular basis, e.g. from a timer interrupt if you have one, or by just continually looping over "JSR SCAN".
It's split into three portions - first, it scans all the keys looking for ones that have changed; then it rescans them as a form of debouncing, as you seem to have been doing something similar already; any keys which have still changed on this second pass then cause calls to KEYUP and KEYDOWN.
Secondly, KEYUP and KEYDOWN handle decisions about when to send events to the synth - they implement the single channel logic, so new keys terminate old ones straight away. These routines feed through to NOTEON and NOTEOFF as appropriate.
Finally NOTEON and NOTEOFF send the right events to the synth. I note that the API here does allow the synth to make its own decisions about how to handle the single channel output - it is possible that you should just make KEYUP always call NOTEON and make KEYDOWN always call NOTEOFF and let the synth itself decide when to start and end notes.
This whole routine would then need to be called on a regular basis, e.g. from a timer interrupt if you have one, or by just continually looping over "JSR SCAN".
Code: Select all
.space CURNOTE 1 ; The index of the currently-playing key, or negative if none
.space KEYSTATES 76 ; Array showing last known states of all keys
.space KEYCHANGED 76 ; Array showing whether each key has changed state this scan
SCAN: LDY #75
SCANLOOP: LDA KYBD, Y ; Read current state of key
EOR KEYSTATES, Y ; Compare against last known state
STA KEYCHANGED, Y ; Store whether it is different in separate array
DEY
BPL SCANLOOP
; An extra delay loop could be inserted here for more debouncing
LDY #75
RESCANLOOP: LDA KEYCHANGED, Y ; See if we thought this key changed state
BNE CHANGED
NOCHANGE: DEY ; No state change, go to next key
BPL RESCANLOOP
RTS ; Scan complete
CHANGED: LDA KYBD, Y ; Re-check the key, as a form of debouncing
EOR KEYSTATES, Y
BEQ NOCHANGE ; Now it hasn't changed state, possibly it was a bouncing issue, ignore it
EOR KEYSTATES, Y ; Get back to the state of the key that was read at CHANGED
STA KEYSTATES, Y ; Update KEYSTATES entry
BEQ RELEASE ; If it was a release event, branch
JSR KEYDOWN ; Handle new pressed key
BRA NOCHANGE ; Return to the RESCAN loop
RELEASE: JSR KEYUP ; Handle released key
BRA NOCHANGE ; Return to the RESCAN loop
KEYUP: CPY CURNOTE ; Is this key the currently-playing note?
BNE DIFFNOTE ; If not, ignore the event
JSR NOTEOFF ; Tell synth to end the currently-playing note
LDA #$FF
STA CURNOTE ; Mark that no note is currently playing
DIFFNOTE: RTS
KEYDOWN: LDA CURNOTE ; Check if a note is playing
BMI NONOTE
JSR NOTEOFF ; Tell synth to end the currently-playing note
NONOTE: STY CURNOTE ; Remember the new note
JSR NOTEON ; Tell synth to start the note
RTS
NOTEON: TYA
ORA #$80
STA REGEN ; Turn gate on with key ID and D7=1
RTS
NOTEOFF: LDA CURNOTE
STA REGEN ; Turn gate off with key ID and D7=0
RTS
Re: Coding for a modular synthesizer keyboard help
Yuri wrote:
Press A, Press B, Press C, release B....
You now have a gap in the middle of the list....
You now have a gap in the middle of the list....
Quote:
Again, if you're actually playing single notes, this isn't something I foresee as being something that would generally happen.
This is all easy enough to deal with: simply keep a list of all keys that are currently pressed, in order. There's no need to use a linked list for this; the keypress list is typically short (maybe four or five for a typing keyboard; and a dozen is probably fine for a piano keyboard) and so you can simply use a short array of bytes which will likely be faster to manipulate than a linked list of any kind, even though you have to copy in order to move chunks of the list. (And there are tricks to reduce the amount of copying you need to do, such as using $00 as an "empty slot" marker so you can do deletions without copies, and then just do a single compaction when your list fills up. But given the speed of a 6502, performance tricks are unlikely to be necessary. Even less so given that this is apparently a 7 MHz 6502, if I was reading the comments right. The keyboard scan is actually going to be taking up much more time than any short array manipulation anyway.)
gfoot wrote:
Regarding what to do with the events - given that the synth only has one channel, I think it's fairly simple:
- On a release event, if it corresponds to the note that is currently playing, end the note. Otherwise do nothing.
- On a press event, if a note is currently playing, end it; and regardless, start the new note.
Curt J. Sampson - github.com/0cjs
-
jimmydmusic
- Posts: 9
- Joined: 09 Sep 2025
Re: Coding for a modular synthesizer keyboard help
This works great! Thanks gfoot!!
gfoot wrote:
I saw you posted the actual code you're using, which is really helpful as it shows how the hardware looks from the software point of view. See below the algorithm I was suggesting, based on how your hardware seems to work.
It's split into three portions - first, it scans all the keys looking for ones that have changed; then it rescans them as a form of debouncing, as you seem to have been doing something similar already; any keys which have still changed on this second pass then cause calls to KEYUP and KEYDOWN.
Secondly, KEYUP and KEYDOWN handle decisions about when to send events to the synth - they implement the single channel logic, so new keys terminate old ones straight away. These routines feed through to NOTEON and NOTEOFF as appropriate.
Finally NOTEON and NOTEOFF send the right events to the synth. I note that the API here does allow the synth to make its own decisions about how to handle the single channel output - it is possible that you should just make KEYUP always call NOTEON and make KEYDOWN always call NOTEOFF and let the synth itself decide when to start and end notes.
This whole routine would then need to be called on a regular basis, e.g. from a timer interrupt if you have one, or by just continually looping over "JSR SCAN".
It's split into three portions - first, it scans all the keys looking for ones that have changed; then it rescans them as a form of debouncing, as you seem to have been doing something similar already; any keys which have still changed on this second pass then cause calls to KEYUP and KEYDOWN.
Secondly, KEYUP and KEYDOWN handle decisions about when to send events to the synth - they implement the single channel logic, so new keys terminate old ones straight away. These routines feed through to NOTEON and NOTEOFF as appropriate.
Finally NOTEON and NOTEOFF send the right events to the synth. I note that the API here does allow the synth to make its own decisions about how to handle the single channel output - it is possible that you should just make KEYUP always call NOTEON and make KEYDOWN always call NOTEOFF and let the synth itself decide when to start and end notes.
This whole routine would then need to be called on a regular basis, e.g. from a timer interrupt if you have one, or by just continually looping over "JSR SCAN".
Code: Select all
.space CURNOTE 1 ; The index of the currently-playing key, or negative if none
.space KEYSTATES 76 ; Array showing last known states of all keys
.space KEYCHANGED 76 ; Array showing whether each key has changed state this scan
SCAN: LDY #75
SCANLOOP: LDA KYBD, Y ; Read current state of key
EOR KEYSTATES, Y ; Compare against last known state
STA KEYCHANGED, Y ; Store whether it is different in separate array
DEY
BPL SCANLOOP
; An extra delay loop could be inserted here for more debouncing
LDY #75
RESCANLOOP: LDA KEYCHANGED, Y ; See if we thought this key changed state
BNE CHANGED
NOCHANGE: DEY ; No state change, go to next key
BPL RESCANLOOP
RTS ; Scan complete
CHANGED: LDA KYBD, Y ; Re-check the key, as a form of debouncing
EOR KEYSTATES, Y
BEQ NOCHANGE ; Now it hasn't changed state, possibly it was a bouncing issue, ignore it
EOR KEYSTATES, Y ; Get back to the state of the key that was read at CHANGED
STA KEYSTATES, Y ; Update KEYSTATES entry
BEQ RELEASE ; If it was a release event, branch
JSR KEYDOWN ; Handle new pressed key
BRA NOCHANGE ; Return to the RESCAN loop
RELEASE: JSR KEYUP ; Handle released key
BRA NOCHANGE ; Return to the RESCAN loop
KEYUP: CPY CURNOTE ; Is this key the currently-playing note?
BNE DIFFNOTE ; If not, ignore the event
JSR NOTEOFF ; Tell synth to end the currently-playing note
LDA #$FF
STA CURNOTE ; Mark that no note is currently playing
DIFFNOTE: RTS
KEYDOWN: LDA CURNOTE ; Check if a note is playing
BMI NONOTE
JSR NOTEOFF ; Tell synth to end the currently-playing note
NONOTE: STY CURNOTE ; Remember the new note
JSR NOTEON ; Tell synth to start the note
RTS
NOTEON: TYA
ORA #$80
STA REGEN ; Turn gate on with key ID and D7=1
RTS
NOTEOFF: LDA CURNOTE
STA REGEN ; Turn gate off with key ID and D7=0
RTS