Coding for a modular synthesizer keyboard help

Programming the 6502 microprocessor and its relatives in assembly and other languages.
User avatar
Yuri
Posts: 371
Joined: 28 Feb 2023
Location: Texas

Re: Coding for a modular synthesizer keyboard help

Post by Yuri »

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.
jimmydmusic
Posts: 9
Joined: 09 Sep 2025

Re: Coding for a modular synthesizer keyboard help

Post by jimmydmusic »

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.
Last edited by jimmydmusic on Mon Sep 22, 2025 12:33 pm, edited 1 time in total.
gfoot
Posts: 871
Joined: 09 Jul 2021

Re: Coding for a modular synthesizer keyboard help

Post by gfoot »

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:
  • 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.
I think that should cover everything and ensure that every time you press a key, a new note immediately starts playing with no delay, which is I think what was wanted. It should also be easy to extend for more polyphony, if you start with this event-based architecture translating the hardware interface into clear press/release events.
John West
Posts: 383
Joined: 03 Sep 2002

Re: Coding for a modular synthesizer keyboard help

Post by John West »

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
Not with the system I was trying to describe.

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
And here's code to remove the key in X

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
If we have only three keys numbered 0, 1, 2, with tail=3 and head=4, your example goes like this:

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)
Whenever you need to know the most recently pressed key, just read next+head. If it's equal to tail, there is no current key.

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
sark02
Posts: 241
Joined: 10 Nov 2015

Re: Coding for a modular synthesizer keyboard help

Post by sark02 »

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.
gfoot
Posts: 871
Joined: 09 Jul 2021

Re: Coding for a modular synthesizer keyboard help

Post by gfoot »

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".

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

Post by jimmydmusic »

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".

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
User avatar
cjs
Posts: 759
Joined: 01 Dec 2018
Location: Tokyo, Japan
Contact:

Re: Coding for a modular synthesizer keyboard help

Post by cjs »

Yuri wrote:
Press A, Press B, Press C, release B....

You now have a gap in the middle of the list....
Right. So close it up, so you have a list indicating that A and C have been pressed, in that order.
Quote:
Again, if you're actually playing single notes, this isn't something I foresee as being something that would generally happen.
Yes, it will commonly happen. Even when not deliberately playing legato, it's not unusual for a new key to be pressed before a current press is released. (This is as true of typing as it is of playing a musical keyboard, as I discovered the hard way on my Basic Master Jr. where the standard BIOS ignores all keypresses until the previous keypress is released.)

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.
This doesn't accommodate a fairly standard technique for monophonic playing, supported by every monophonic synth controlled by a keyboard that I've ever used, which is to hold down one key and switch between that and a higher one by pressing and releasing the higher one. E.g., hold C and, while doing that, press and release D several times to play C-D-C-D-C-D-C with no break between the notes. I don't know how important other folks find this, but I would find it quite annoying not to be able to do this.
Curt J. Sampson - github.com/0cjs
jimmydmusic
Posts: 9
Joined: 09 Sep 2025

Re: Coding for a modular synthesizer keyboard help

Post by jimmydmusic »

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".

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
Post Reply