Cv/gate/midi

Modding and Technical Issues

Moderator: Moderators

User avatar
R'zo
Vic 20 Nerd
Posts: 514
Joined: Fri Jan 16, 2015 11:48 pm

Re: Cv/gate/midi

Post by R'zo »

@chysn
Are you planning on providing in/out with this or only outs?
Any chance of having separate cv/gate lines so this could be used with modules without an external midi interface?

If there's anything that I can do to help you on this let me know.
R'zo
I do not believe in obsolete...
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

R'zo wrote: Mon Aug 30, 2021 8:42 am @chysn
Are you planning on providing in/out with this or only outs?
Any chance of having separate cv/gate lines so this could be used with modules without an external midi interface?
I plan on sort of building a user port/Arduino interface up in stages. First job is to make sure that I have command of the user port I/O by using a serial monitor. The next step is MIDI out, because the hardware is really easy. Then CV out with the I2C DAC and a gate with a 5V digital out.

My initial plan is for CV and gate to go through MIDI. The CV will be sent by software as a proportioned continuous controller on a programmable MIDI channel, and Gate will monitor note on and off for a programmable MIDI channel. Maybe the same channel. The user wouldn't necessarily need to know how this works, but it will provide a consistent code interface for VIC-20 software.

Then MIDI in, then (maybe) CV in.

Obviously, part of this will be VIC-20 ML subroutines for sending and receiving. The advantage of putting the Arduino in the middle is that it can take care of a lot of tasks that would normally complicate VIC-20 code, like MIDI buffering and timing.
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

Like pretty much all projects I undertake, I start with trivial baby steps, and then weave them into less trivial steps as I go.

Today, I attached a header to a 24-pin project PCB with some little bits of hookup wire. Right now, I care only about the A through N pins on the bottom of the user port:
IMG_4713.jpg
IMG_4714.jpg

Then, I plugged this into my VIC-20 and wrote a little program that sets the DDR to all-output, and then POKE to the port:

Code: Select all

10 POKE 37138,255
20 C=1
30 POKE 37136,C:PRINT C
40 GET A$:IF A$ = "" THEN 40
50 C=C*2
60 GOTO 30
IMG_4707.jpg
Then I hook up the ground wire (N) and one of the data lines to a meter and run the program. The expected lines have low voltage:
IMG_4709.jpg
But the hooked-up line has high voltage:
IMG_4708.jpg
So the user port is working like I thought it would. The next step will be to connect all the data and control lines to an Arduino Nano and watch the serial monitor for changes. If that works okay, then MIDI out is next. I should have a DIN MIDI circuit already assembled somewhere. If I can't find it, I'll just use TRS MIDI.
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

Tests with the Nano are encouraging. I connected only five lines because I didn't want to schlepp downstairs for more wires.

The reason I'm not testing with the ESP-32 is stupid. It's because the thing is too damn wide for the breadboards I have. But for my CVCast idea, I'll need ESP-32 for its WiFi. But Nano is perfectly serviceable for proofs-of-concept.
IMG_4715.jpg
The Arduino sketch looks like this:

Code: Select all

const int VPB0 = 12;
const int VPB1 = 11;
const int VPB2 = 10;
const int VPB3 = 9;
const int VPB4 = 8;
int last = 0;

void setup() {
    Serial.begin(9600);
    pinMode(VPB0, INPUT);
    pinMode(VPB1, INPUT);
    pinMode(VPB2, INPUT);
    pinMode(VPB3, INPUT);
    pinMode(VPB4, INPUT);
    Serial.print("READY.\n\r");
}

void loop() {
        int val = digitalRead(VPB0) +
                  digitalRead(VPB1) * 2 + 
                  digitalRead(VPB2) * 4 + 
                  digitalRead(VPB3) * 8 + 
                  digitalRead(VPB4) * 16;
        if (val != last) {                  
            Serial.print(val);
            Serial.print("\n\r");
            last = val;
        }
}
I run this, then open the serial monitor, and make sure that the numbers I'm POKEing on the VIC are the numbers I'm seeing in the serial monitor. And, yay, it works!

However, I need to learn about how the CB1 and CB2 handshaking works, because right now my timing is based on whenever the sum changes, and I occasionally capture intermediate values. The handshaking should solidify that, and I'll be able to design a protocol. This is something I want to talk to you about, Ryan, so we can have a simple but expressive code interface. I'll PM you about this.

I'm pretty confident that I'll be demoing MIDI out within a few days.
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

All right, last post of the night, I promise. I had to read the Programmer's Reference Guide's User Port chapter like six times before I really started to get it. But everything I need to know is right there. I'm using the Handshake Output mode, which involves connecting CB2 as an input on the Arduino, and CB1 as an output.

When I do a write to the port ($9110), the VIA sets the CB2 line low. The Arduino is watching this line, and reads the port lines when the CB2 line goes low. It then sends a transition on CB1. The VIA sees this transition and resets CB2 high. This change in state (on CB1) acts as an acknowledgement that the port has been read by the Arduino. Setting CB2 high again causes the Arduino to ignore the port lines until the port has been written to, so we don't get transitional data.

The Arduino sketch now looks like this

Code: Select all

// Data lines (bits 0-3)
const int VPB0 = 12;
const int VPB1 = 11;
const int VPB2 = 10;
const int VPB3 = 9;

// Control lines
const int VCB1 = 2;
const int VCB2 = 3;

void setup() {
    Serial.begin(9600);
    // Data lines
    pinMode(VPB0, INPUT);
    pinMode(VPB1, INPUT);
    pinMode(VPB2, INPUT);
    pinMode(VPB3, INPUT);

    // Control lines
    pinMode(VCB1, OUTPUT); // Set LOW to acknowledge data received
    pinMode(VCB2, INPUT); // Reads LOW when data received
    
    Serial.print("READY.\n\r");
}

void loop() {
    if (!digitalRead(VCB2)) {
        int val = digitalRead(VPB0) +
            digitalRead(VPB1) * 2 + 
            digitalRead(VPB2) * 4 + 
            digitalRead(VPB3) * 8;
            Serial.print(val);
            Serial.print("\n\r");
            digitalWrite(VCB1, LOW);  // Acknowledge with transition on CB1
            digitalWrite(VCB1, HIGH);
    }
}
And the VIC-20 test program looks like this:

Code: Select all

10 POKE 37138,255
20 POKE 37148,128
30 INPUT "VALUE (0-15)";V
40 POKE 37136,V
50 GOTO 30
Line 20 is the new part; it sets the Peripheral Control Register to Handshake Output Mode, which works as I described above.

So, all right, this is getting fun!
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

Here's the sketch for MIDI:

Code: Select all

// Data lines (bits 0 - 7)
const int VPB0 = 12;
const int VPB1 = 11;
const int VPB2 = 10;
const int VPB3 = 9;
const int VPB4 = 8;
const int VPB5 = 7;
const int VPB6 = 6;
const int VPB7 = 5;


// Control lines
const int VCB1 = 2;
const int VCB2 = 3;

void setup() {
    Serial.begin(31250);
    // Data lines
    for (int b = 0; b < 8; b++)
    {
        pinMode(VPB7 + b, INPUT);
    }

    // Control lines
    pinMode(VCB1, OUTPUT); // Transition to acknowledge data was received
    pinMode(VCB2, INPUT); // Reads LOW when data received
}

void loop() {
    if (!digitalRead(VCB2)) {
        int out = 0;
        int val = 256;
        for (int i = 0; i < 8; i++)
        {
            val /= 2; // Power of 2, descending
            int b = 7 - i; // b is bit number
            int pin = i + VPB7; // Physical pin number
            out += digitalRead(pin) * val;
        }
        Serial.write(out);
        digitalWrite(VCB1, LOW);  // Acknowledge with transition on CB1
        digitalWrite(VCB1, HIGH);
    }
}
As you can see, I've now got all eight data lines hooked up to the user port. I'm using the same type of handshaking as before. The main difference now is that I've got a MIDI output attached. Now, a MIDI output was documented in the original 1983 draft specification, and it's just three lines: +5V, Ground, and a data line. The +5V and data line each go through a 220-Ohm resistor.

I'm using a tip-ring-sleeve minijack, like Korg uses for their tiny synths. I've built 5-pin DIN interfaces before, and they're a bit clunky for prototypes. So my MIDI out is this:
IMG_4725.jpg
That's connected to the 5V, ground, and TX pin on the Nano. And here are all the other data pins hooked up:
IMG_4720.jpg
I've got a TRS-to-MIDI cable (there are a couple varieties of these, and I use the Korg style), and then a MIDI cable continues out to my little portable synth, an Arturia MicroBrute:
IMG_4721.jpg
On the VIC-20, my proof-of-concept was this BASIC program:

Code: Select all

10 POKE 37138,255
20 POKE 37148,128
30 S = 144
35 FOR N = 48 TO 60
37 PRINT "NOTE #"N"{CRSR UP}"
40 POKE 37136,S
41 POKE 37136,N
42 POKE 37136,128
45 FORD=1TO50:NEXTD
50 POKE 37136,128
51 POKE 37136,N
52 POKE 37136,0
60 NEXT N
70 GET A$:IF A$ = "" THEN 35
Lines 40-42 are the MIDI Note On command. Three bytes are sent, the status byte $90 (Note On, Channel 1), the note number, and the velocity (a measure of how hard a note was played, which is typically mapped to volume, sometimes to timbre). Then there's a short delay. Lines 50-52 are the MIDI Note Off command. By spec, it's a separate status byte ($80 for Channel 1), but most synths treat Note On with a velocity of 0 as an alias for Note Off.

(I'm checking for a keypress to make sure that Note Off is sent, because the MicroBrute is monophonic, and it gets confused if another Note On arrives before the Note Off.)

Here's a video of the thing in action. The waterfall you hear in the background is my son's turtle's filter. https://youtu.be/pZIafBW4LmI

Next step? I'm going to work on the software side for a bit. Everything's been in BASIC so far, and I want to write a MIDI KERNAL.
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

Here's the first version of the MIDI KERNAL. It has tools for sending all of the MIDI commands, plus a generic byte send for special things like system exclusive messages. Input support is what I'll work on next... or maybe CV out next, as the spirit moves me. https://github.com/Chysn/MIDI-KERNAL/bl ... kernal.asm

The MIDI KERNAL doesn't need to support CV, because that will be handled by the Arduino software and hardware. So MIDI KERNAL is pretty singular in its purpose; I feel really good about my decision to make a MIDI interface that just uses actual MIDI.

Here's a demo that makes use of the MIDI KERNAL to make a little piano-type controller application. Hopefully this shows how dead-simple it is.

Code: Select all

; This is a demonstration of the MIDI KERNAL
; Play keys to send MIDI Note On commands, and release them to send Note Off
; Recognized keys are
;     A  S  D  F  G  H  J  K
; which play a C major scale from Middle C (#60)
; Additionally, F1 toggles between MIDI output and CV output

; Memory
KEYDOWN     = $c5               ; Which key is being held?
LASTKEY     = $fa               ; What key was played last?

* = $1600
Start:      jmp Main

#include "midikernal.asm"

Main:       jsr SETOUT          ; Set up port for MIDI out
            lda #0              ; Channel 1
            jsr SETCH           ; ,,
start:      lda KEYDOWN         ; Wait for a key press
            cmp #$40            ; ,,
            beq start           ; ,,
            sta LASTKEY         ; Take note of key press
            cmp #39             ; If F1 is pressed, alternate between
            bne keyboard        ;   channel 1 and channel 16 (MIDI/CV)
            lda #$0f            ;   ,,
            eor MIDICH          ;   ,,
            sta $900f           ;   ,, Screen color indicator
            jsr SETCH           ;   ,,
            jmp end             ;   ,, 
keyboard:   ldy #7              ; Search the keyboard table for a valid
search:     lda KeyTable,y      ;   note
            cmp LASTKEY         ; Is this table entry the key pressed?
            beq found           ; If so, go play the note
            dey                 ; Keep searching all 8 table entries
            bpl search          ; ,,
            bmi start           ; The pressed key isn't here, so wait again
found:      lda NoteTable,y     ; Get the MIDI note number at found index
            tax                 ; Set X for Note On call
            ldy #100            ; Set Y as velocity
            jsr NOTEON          ; Send Note On command at specified channel
end:        lda KEYDOWN         ; Keep playing the note until the key is
            cmp LASTKEY         ;   released
            beq end             ;   ,,
            jsr NOTEOFF         ; Once the key is released, send Note Off
            jmp start           ; Back to note start
            
; Key codes for A,S,D,F,G,H,J,K            
KeyTable:   .byte 17,41,18,42,19,43,20,44

; Note numbers for C Major from Middle C
NoteTable:  .byte 60,62,64,65,67,69,71,72
And a minor hardware-side update: The User Port's pin 2 supplies 5V at 100mA, which is plenty to power the Nano, so I hooked this up to get completely away from the computer.
Last edited by chysn on Sat Sep 04, 2021 11:16 pm, edited 1 time in total.
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

CV Out and Gate are done. Here's the new interface board:
IMG_4730.jpg
From left to right (er... bottom to top): TRS MIDI output, Gate Out, CV Out.

CV Out uses an MCP4725 I2C 12-bit DAC. I love I2C because I could build an interface with bunches of these without running out of pins on the microcontroller. But for now, just one. It has a range of 0 to (slightly under) 5V, which is an iffy range for Eurorack, but not uncommon.

Here's the full thing:
IMG_4731.jpg
I'm not a fan of the rat king look, but it'll get worse before it gets better. I'll solder up a cleaner permanent unit once everything is done. I have no idea what to do about a housing at this point.

Here's the Arduino sketch that supports CV/Gate/MIDI out:

Code: Select all

#include <Adafruit_MCP4725.h>
#include <EEPROM.h>

// Data lines (bits 0 - 7)
// Start at bit 7 (pin 5), and end at bit 0 (pin 12)
const int UPORT7 = 5;

// Control lines
const int VCB1 = 2;
const int VCB2 = 3;

// CV Gate
const int GATEOUT = 4;

// CV 
Adafruit_MCP4725 dac;
int volt_ref; // Default calibration
int cv_channel;       // MIDI channel for MIDI
int curr_status = 0;  // Current MIDI status
int curr_channel = 0; // MIDI channel number of current status
int data_count = 0;   // Number of MIDI data bytes since last status change
int data1 = 0;        // Value of first MIDI data byte
int sysex[8];         // System exclusive message
int sysex_ix;         // Sysexm exclusive index

/* This is the starting point for DAC steps-per-volt calibration. It's experimentally-determined
 *  with my Nano and my meter, but I've noticed that it varies according to power supply. So
 *  in real life, you'll want to calibrate your interface
 */
const int DEFAULT_VOLT_REF = 876; // Calibration of DAC at 1V

void setup() {
    Serial.begin(31250);
    //Serial.begin(9600); // Diagnostics

    // Data lines - default to input
    for (int b = 0; b < 8; b++) pinMode(UPORT7 + b, INPUT);

    // Control lines
    pinMode(VCB1, OUTPUT); // Set LOW to acknowledge data received
    pinMode(VCB2, INPUT); // Reads LOW when data received

    // CV Setup
    dac.begin(0x60);
    int msb = EEPROM.read(0); // This calibration data on the EEPROM is big-endian
    int lsb = EEPROM.read(1);
    volt_ref = (msb * 256) + lsb;
    if (volt_ref > 2048 || volt_ref < 256) volt_ref = DEFAULT_VOLT_REF;
    cv_channel = 15; // Default CV binding to channel 16
}

void loop() {
    if (!digitalRead(VCB2)) {
        int out = 0;
        int val = 256;
        for (int i = 0; i < 8; i++)
        {
            val /= 2; // Power of 2, descending
            int b = 7 - i; // b is bit number
            int pin = i + UPORT7; // Physical pin number
            out += digitalRead(pin) * val;
        }
        Serial.write(out);
        digitalWrite(VCB1, LOW);  // Acknowledge with transition on CB1
        digitalWrite(VCB1, HIGH);

        // Handle CV and Gate. Note On on the CV channel will set gate high, and Note Off on the
        // CV channel will set gate low. Start of Pitch Bend on the CV channel starts data collection
        // of the next two bytes, LSB and MSB, and turns them into a 14-bit value, which is then
        // dithered down to a 12-bit value for sending to the DAC.
        if (out >= 0x80) {
            // This is a MIDI status byte
            curr_status = out & 0xf0;
            curr_channel = out & 0x0f;
            data_count = 0;

            if (out == 0xf0) sysex_ix = 0; // Start of exclusive

            if (out == 0xf7) { // End of exclusive
                // Check for system exclusive signature for this product
                if (sysex[1] == 0x7d && sysex[2] == 0x62 && sysex[3] == 0x76) {
                    // 7d = Non-commercial manufacturer ID
                    // 62 = "B" Beige Maze
                    // 76 = "V" VIC-20 Products
                    if (sysex[4] == 0x6d) { // M binds MIDI channel to CV/Gate
                        cv_channel = sysex[5] & 0x0f;
                    }

                    if (sysex[4] == 0x63) { // C sets 1V calibration value LSB, MSB
                        int lsb = sysex[5]; // 7-bit values from MIDI
                        int msb = sysex[6];
                        volt_ref = (msb * 128) + lsb;
                        int low = volt_ref % 256;  // Convert MIDI data bytes to 8-bit high and
                        int high = volt_ref / 256; // low bytes
                        EEPROM.write(0, high); // Calibration data is big-endian
                        EEPROM.write(1, low);
                    }
                }
            }
            
        } else {
            if (curr_channel == cv_channel) {
                data_count++;
                if (data_count == 1) data1 = out;
                if (data_count == 2) {
                    // Handle Gate On (Note on)
                    if (curr_status == 0x90) { // Note on
                        digitalWrite(GATEOUT, HIGH);
                        if (out) { // Quantize CV if velocity > 0
                            // MIDI Note #36 = C 0V
                            //           #48 = C 1V
                            //           #60 = C 2V
                            //           #72 = C 3V
                            //           #84 = C 4V
                            if (data1 >= 36 && data1 <= 96) {
                                int note = data1 % 12; // Get note where C = 0
                                int oct = (data1 - 36) / 12; // Get octave number
                                int value = (oct * volt_ref) + ((volt_ref / 12)  * note);
                                dac.setVoltage(value, false);
                            }
                        }
                    }

                    // Handle Gate off (Note off)
                    if (curr_status == 0x80) { // Note off
                        digitalWrite(GATEOUT, LOW);
                    }

                    // Handle CV output for 12-bit DAC value
                    if (curr_status == 0xe0) { // Pitch bend (DAC output)
                        int value = (out * 128) + data1;
                        if (value >  4095) value = 4095; // Enforce 12-bit value
                        dac.setVoltage(value, false);
                    }

                    // Handle quantized CV independent of Gate (using CC#3)
                    if (curr_status == 0xb0) { // Control change
                        if (data1 == 3) { // Quantize CV if CC#3
                            if (out >= 36 && out <= 96) {
                                int note = out % 12; // Get note where C = 0
                                int oct = (out - 36) / 12; // Get octave number
                                int value = (oct * volt_ref) + ((volt_ref / 12)  * note);
                                dac.setVoltage(value, false);
                            }
                        }
                    }
                }
            }            
        }

        // Add to system exclusive buffer if sysex status is on
        if (curr_status == 0xf0) sysex[sysex_ix++] = out;
    }
}
On the VIC-20 side, CV and Gate just use the MIDI KERNAL. By default, the CV and Gate are bound to MIDI channel 16, but this can be changed with a system exclusive message (see sample code below).

Here's sample code for sending Gate and CV using the MIDI KERNAL, as well as changing the MIDI channel binding.

Note: I'm going to be updating this code on this post as new features are added.

Code: Select all

; Configure for CV/Gate
CVDemo:     jsr SETOUT          ; Configure port for MIDI Out
            lda #CV_CHANNEL     ; Set to configured CV channel
            jsr SETCH           ; ,,

; Set 5V Gate On            
GateOn:     ldy #0              ; Note On with 0 velocity will set Gate On
            jsr NOTEON          ;   In this case, X has no function

; Set 5V Gate Off
GateOff:    jsr NOTEOFF         ; And also for Gate Off

; Send DAC Value to CV Out
CVOut:      ldx CV              ; CV is a 16-bit value with a 12-bit range. It
            txa                 ;   needs to be converted to 7-bit MIDI data
            asl                 ; Send bit 7 of low byte over to the MSB
            lda CV+1            ; ,,
            rol                 ; ,,
            and #%00011111      ; Keep 5 bits (the other 7 will be in the LSB)
            tay                 ; PITCHB expects MSB in Y
            jsr PITCHB          ; Send CV with MIDI LSB in X and MSB in Y

; Send Quantized Note Value to CV Out and Turn On Gate        
Quantize:   ldx #NOTE_NUMBER    ; Send Note on with note between 36 and 96,
            ldy #NONZERO        ;   and a nonzero velocity to quantize the CV
            jsr NOTEON          ;   output
            
; Send Quantized Note Value to CV Out, Independent of Gate       
QuantOnly:  ldx #3              ; Send a control change message on CC#3
            ldy #NOTE_NUMBER    ;   with note number in Y
            jsr CONTROLC
            
; Bind MIDI Channel to CV Output
; Use this to change the binding from the default of 16
BindCVCH:   ldy #5              ; Send system exclusive header for channel
-loop:      lda CHSYSEX,y       ;   binding
            jsr MIDIOUT         ;   ,, (Send raw data to MIDI port)
            dey                 ;   ,,
            bpl loop            ;   ,,
            lda #CV_CHANNEL     ; This is the new MIDI channel to bind to CV
            jsr MIDIOUT         ; ,,
            lda #$f7            ; End-of-Exclusive
            jsr MIDIOUT         ; ,,
; SysEx header in reverse. Start of SysEx ($f0), Non-Commercial ID ($7d),
;                          Beige Maze ($62), VIC-20 Projects ($76),
;                          MIDI Channel Set ($6d)
CHSYSEX:    .byte $6d,$76,$62,$7d,$f0
The CVOut code isn't that tricky. We must convert a 12-bit DAC value in memory into a pair of MIDI data bytes, which are a pair of 7-bit values. So CVOut takes the lowest 7 bits for the LSB, and the highest 5 bits for the MSB. To do this, it shifts bit 7 of the LSB over to bit 0 of the MSB. You may notice that we're not stripping bit 7 out of X. That's because the MIDI KERNAL does this for us, to preserve the integrity of MIDI status messages.

If you want to send a note, rather than a raw DAC value, see the Quantize and QuantOnly examples. For Quantize, you send a note number between 36 and 96 (in X) and a nonzero velocity (in Y) to NOTEON. This also sets the Gate On. If you send CC#3 with the note number, the CV output will be quantized, but the Gate will remain in its current state.

If you use quantization, you'll want to provide a DAC calibration screen in your software. Example calibration code is a couple posts down.

I have a busy as hell weekend (a holiday in U.S.A.), but I'll post a video demo when I can.
Last edited by chysn on Sun Sep 05, 2021 11:15 am, edited 9 times in total.
User avatar
R'zo
Vic 20 Nerd
Posts: 514
Joined: Fri Jan 16, 2015 11:48 pm

Re: Cv/gate/midi

Post by R'zo »

Wow you work quickly! Looking very nice so far. I can't wait to start playing around with this.

I've been looking at $9120/$9121 hoping to get multiple key input from the keyboard. It would be nice if I could set up half the keys for playing the vic and then some for controlling midi triggers, a second playable midi keyboard etc. through midi out but it is looking like reading writing to this area might interfere with your interrupt.
R'zo
I do not believe in obsolete...
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

R'zo wrote: Sat Sep 04, 2021 2:44 pm Wow you work quickly! Looking very nice so far. I can't wait to start playing around with this.

I've been looking at $9120/$9121 hoping to get multiple key input from the keyboard. It would be nice if I could set up half the keys for playing the vic and then some for controlling midi triggers, a second playable midi keyboard etc. through midi out but it is looking like reading writing to this area might interfere with your interrupt.
There won't be any barriers to that, as far as I can see. You'll be able to check the type of interrupt request for every interrupt.

Note that I've changed the CV output syntax shown above. I wanted the interface to support two types of CV output: raw 12-bit DAC values (0-4095), and quantized notes (based on note number). So I added syntax to express that.

I'm just going to update the example code in the post above. I added examples for note quantization (that is, sending CV based on a MIDI note number rather than a raw DAC value), and CV channel binding. The new version binds channel 16 to CV, but you can send a system exclusive message to change that.
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

Here's the calibration procedure. I need to recalibrate depending on whether I'm using my MacBook Pro or the VIC-20 to power the interface.

When my VIC-20 is powering the interface, the CV range tops out at about 3.75 volts.

But VICs and DACs can differ from unit to unit. If you plan to use quantized CV with this interface, you'll need to provide the user with a routine similar to this, probably with better UI :) If you only plan to use raw DAC values for your CV functions, you don't need to include calibration.

I wouldn't worry about user barriers to calibration. I think it's safe to assume that every single person who uses CV for music has a meter, and has done this kind of thing before. It's part of the territory, especially since they'll probably be assembling the interface.

https://youtu.be/mi-zd2nyoyo

Code: Select all

; 12-Bit DAC 1V Calibration
;
; The accuracy of the DAC seems to depend on its power source.
; Connect a meter to CV out. The target is 1V.
; If the CV is lower than 1V, use F1 and F3 to raise the voltage
; If the CV is higher than 1V, use F7 and F5 to lower the voltage
; When the meter reads 1V (or as close as possible to it),
; press RETURN. This will set the calibration in the interface, and
; store the calibration value to EEPROM for the next power cycle.
;
; Dependency --
; Note that the MIDI KERNAL is included at the end of this file
KEYDOWN     = $c5
CALIB       = $fa               ; Current calibration value (2 bytes)
BURN        = $fc               ; Value as MIDI data bytes (2 bytes)
PRTFIX      = $ddcd             ; Decimal display routine (A,X)
CHROUT      = $ffd2             ; Character out

; Key Adjust Constants
F1          = 39                ; Coarse up
F3          = 47                ; Fine up
F5          = 55                ; Fine down
F7          = 63                ; Coarse down
RETURN      = 15                ; Write to EEPROM and exit

* = $1800
Main:       jsr SETOUT          ; Configure MIDI output
            lda #$0f            ; Set channel to CV
            jsr SETCH           ; ,,
            lda #$00            ; Starting point for calibration is
            sta CALIB           ; $0400. This is just a guess, based on
            lda #$04            ; a few samples of the DAC.
            sta CALIB+1         ; ,,
show:       ldx CALIB           ; Show current DAC value
            lda CALIB+1         ; ,,
            jsr PRTFIX          ; ,,
            lda #$0d            ; ,,
            jsr CHROUT          ; ,,
send:       ldx CALIB           ; CALIB is a 16-bit value with a 12-bit range.
            txa                 ;   It needs to be converted to 7-bit MIDI data
            asl                 ; Send bit 7 of low byte over to the MSB
            lda CALIB+1         ; ,,
            rol                 ; ,,
            and #%00011111      ; Keep 5 bits (the other 7 will be in the LSB)
            tay                 ; PITCHB expects MSB in Y
            jsr PITCHB          ; Send CV with MIDI LSB in X and MSB in Y    
-debounce:  lda KEYDOWN         ; Debounce key
            cmp #$40            ; ,,
            bne debounce        ; ,,
adjust:     lda KEYDOWN         ; Wait for key press
            cmp #$40            ; ,,
            beq adjust          ; ,,
            cmp #RETURN         ; If RETURN has been pressed, commit the
            beq commit          ;   new CV value to EEPROM
            ldy #4              ; Check for each of the adjustment keys (F1-F7)
search:     cmp KeyTable,y      ; ,,
            beq found           ; ,, If the key has been found, do adjustment
            dey                 ; ,,
            bpl search          ; ,,
            bmi adjust          ; If the pressed key isn't on the list, go back
found:      lda LowAdd,y        ; Get the fine or coarse adjustment values from
            clc                 ;   the table, depending on the key index, and
            adc CALIB           ;   adjust the 16-bit calibration value as
            sta CALIB           ;   indicated in the table
            lda HighAdd,y       ;   ,,
            adc CALIB+1         ;   ,,
            sta CALIB+1         ;   ,,
            jmp show            ; Show the new voltage
commit:     lda CALIB           ; Convert the 16-bit calibration value into
            sta BURN            ;   two 7-bit values for sending over MIDI
            asl                 ;   ,,
            lda CALIB+1         ;   ,,     
            rol                 ;   ,,
            and #%00011111      ;   ,,
            sta BURN+1          ;   ,,
            ldy #4              ; Send the system exclusive header for
-loop:      lda SysEx,y         ;   calibration commit
            jsr MIDIOUT         ;   ,,
            dey                 ;   ,,
            bpl loop            ;   ,,
            lda BURN            ; Send the calibration values
            jsr MIDIOUT         ; ,,
            lda BURN+1          ; ,,
            jsr MIDIOUT         ; ,,
            lda #$f7            ; End of exclusive and return
            jmp MIDIOUT         ; ,,

#include "midikernal.asm"

KeyTable:   .byte F1,F3,F5,F7
LowAdd:     .byte $20,$01,$ff,$e0
HighAdd:    .byte $00,$00,$ff,$ff
SysEx:      .byte $63,$76,$62,$7d,$f0
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

I won't have MIDI in for a while because I don't have the right optoisolators. I have one kind, but I could never get them working, so I've ordered some more. It's probably a project for next weekend.

However, I can generate fake MIDI input, so I'll have a head-start on the MIDI KERNAL updates for input by the time I get my chips.
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

Here's a demo of the MIDI output with a pair of synthesizers.

https://www.youtube.com/watch?v=APP8XOff-jo

This time, I'm bringing my Sequential into the mix on channel 2, and the MicroBrute on channel 1.

The software is an adaptation of my two-voice shift register player for ZEPTOPOLIS. It just sends MIDI via the MIDI KERNAL instead of playing the VIC-20 voices.

Code: Select all

; Music Player Memory
MUSIC_REG   = $f9               ; Music shift register (4 bytes)
MUSIC_FL    = $fd               ; Bit 7 set if player is running
MUSIC_TIMER = $fe               ; Music timer
MUSIC_MOVER = $ff               ; Change counter
LAST_CH1    = $02a1             ; Last note, ch 1
LAST_CH2    = $02a2             ; Last note, ch 2

* = $1800
Install:    sei
            lda #<ISR
            sta $0314
            lda #>ISR
            sta $0315
            cli
            jsr SETOUT          ; See MIDI KERNAL (included at bottom)
            jmp MusicInit

ISR:        jsr MusicServ       ; Service music player
            jmp $eabf           ; Return from interrupt

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;  
; MUSIC PLAYER SERVICE
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Service Music
MusicServ:  bit MUSIC_FL        ; Skip if play flag is off
            bpl music_r         ;   ,,
            dec MUSIC_TIMER     ; Fetch new note when timer hits 0
            bmi musicsh
            rts
musicsh:    lda #1              ; Turn off channel 2 note (mid)
            ldx LAST_CH2        ; ,,
            jsr NOTEOFF         ; ,,
            asl MUSIC_REG       ; Shift 32-bit register left
            rol MUSIC_REG+1     ; ,,
            rol MUSIC_REG+2     ; ,,
            rol MUSIC_REG+3     ; ,,
            lda #0              ; Put Carry into A bit 0
            rol                 ; ,,
            ora MUSIC_REG       ; And put that back at the beginning
            sta MUSIC_REG       ; ,,
            dec MUSIC_MOVER     ; When this counter hits 0, alter the music
            bne FetchNote       ;   by flipping bit 0 of registers 1 and 3
            lda MUSIC_REG+1     ;   ,,
            eor #$01            ;   ,,
            sta MUSIC_REG+1     ;   ,,
            lda MUSIC_REG+3     ;   ,,
            eor #$01            ;   ,,
            sta MUSIC_REG+3     ;   ,,
            lda #$7f            ; Reset the mover for a little less than 4 loops
            sta MUSIC_MOVER     ;   per pattern so it goes longer without repeat
FetchNote:  lda Tempo           ; Reset the timer
            sta MUSIC_TIMER     ; ,,
            bit MUSIC_REG       ; A high note played when bit 7 of byte 0 is
            bpl play_high       ;   clear
            lda #0              ; Turn off channel 1 note (high)
            jsr SETCH           ; ,,
            ldx LAST_CH1        ; ,,
            jsr NOTEOFF         ; ,,            
            jmp play_low        ; 
play_high:  lda MUSIC_REG+1     ; Get the mid note
            and #%00001110      ; Mask the low three bits and shift, then
            lsr                 ;   transfer the bits to
            tay                 ;   Y to be the mode degree index
            lda MIDI,y          ; Get the modal note
            tax                 ; Put in X
            ldy #100
            lda #0
            jsr SETCH
            jsr NOTEON
            stx LAST_CH1        ; Save for note off
play_low:   lda MUSIC_REG+1     ; Play the middle register, same byte as the
            and #%00000111      ;   hight, but one bit behind
            tay                 ; Y is the mode degree index
            lda #1              ; Channel 2
            jsr SETCH
            lda MIDI,y
            tax
set_vol:    lda MUSIC_REG+3     ; Set the velocity based on one of the
            and #$0f            ;   registers.
            tay                 ;   ,,
            jsr NOTEON          ;   ,,
            stx LAST_CH2        ; Save note for note off
music_r:    rts

; Stop Music
MusicStop:  lsr MUSIC_FL        ; Turn off Music flag
            rts

; Initialize Music
MusicInit:  ldx #3
-loop:      lda Theme,x
            sta MUSIC_REG,x
            dex
            bpl loop
            lda #0
            sta MUSIC_MOVER
            jsr FetchNote
            ; Fall through to MusicPlay

; Play Music            
MusicPlay:  sec
            ror MUSIC_FL
            rts
            
Theme:      .byte $33,$44,$55,$66
Tempo:      .byte 10
MIDI:       .byte 62,64,65,67,69,71,72,74  

#include "../../MIDI-KERNAL/src/midikernal.asm"
In other news, I got my optoisolators today for MIDI input. I'm waiting for 5-pin DINs, though, before putting it all together. I've finished the MIDI KERNAL code to support input, and I like the way it feels to code. I think it provides an elegant way to hand off interrupt-generated messages to a main loop, its syntax complements the MIDI Out symmetrically, and it handles running status automatically, so the developer doesn't even need to think about it.

I anticipate having to solve some hardware problems with MIDI In, though. I just hope it's not too slow.
User avatar
chysn
Vic 20 Scientist
Posts: 1205
Joined: Tue Oct 22, 2019 12:36 pm
Website: http://www.beigemaze.com
Location: Michigan, USA
Occupation: Software Dev Manager

Re: Cv/gate/midi

Post by chysn »

All right, it took a few days for the parts to arrive, but I've got MIDI In working on a breadboard:



I have not tested the MIDI KERNAL with all message types yet. That'll be sort of a painstaking process that I'll go through next week. I'm also not sure whether my synth is using running status, so I need to figure that out for testing purposes.

Here's the code for the mini synth in use here. Note that it requires the MIDI KERNAL, which is updated here https://github.com/Chysn/MIDI-KERNAL/bl ... kernal.asm

I hope this makes the basic message handling pretty clear.

Code: Select all

; This is a demonstration of the MIDI KERNAL for MIDI input
; It shows how MIDI messages are constructed in an interrupt (via MAKEMSG)
; and handed off to a main loop (via GETMSG), and subsequently handled by
; looking at the message data in A,X, and Y
;
; Note that the MIDI KERNAL is included at the bottom of this file, so make
; sure it's available to the assembler.

; VIC Registers
VOLUME      = $900e             ; Volume Register
VOICE       = $900b             ; Middle Voice Register

; Program Memory
LAST_NOTE   = $fe               ; Last note played

* = $1600
; Installation routine
Install:    lda #<ISR           ; Set the location of the NMI interrupt service
            sta $0318           ;   routine, which will capture incoming MIDI
            lda #>ISR           ;   messages. Note the lack of SEI/CLI here.
            sta $0319           ;   They would do no good for the NMI.
            jsr SETIN           ; Prepare hardware for MIDI input
 
; Main Loop
; Waits for a complete MIDI message, and then dispatches the message to
; message handlers. This dispatching code and the handlers are pretty barbaric.
; In real life, you probably won't be able to use relative jumps for everything.
Main:       jsr GETMSG          ; Has a complete MIDI message been received?
            bcc Main            ;   If not, just go back and wait
            cmp #ST_NOTEON      ; Is the message a Note On?
            beq NoteOnH         ; If so, handle it
            cmp #ST_NOTEOFF     ; Is it a Note Off?
            beq NoteOffH        ; If so, handle it
            bne Main            ; Go back and wait for more

; Note Off Handler            
NoteOffH:   cpx LAST_NOTE       ; X is the note. Is it the last one played?
            bne Main            ; If not, leave it alone
            lda #0              ; Otherwise, silence the voice
            sta VOICE           ; ,,
            jmp Main            ; Go get more MIDI

; Note On Handler  
; For the purposes of this demo, we're just accepting notes on any channel.
; In a real application, you'll probably want to check channel numbers, either
; for accept/reject purposes, or to further dispatch messages. That code would
; look something like this:
;     jsr GETCH
;     cmp #LISTEN_CH
;     beq ch_ok
;     jmp Main   
NoteOnH:    txa                 ; Put note number in A
            sta LAST_NOTE       ; Store last note for Note Off
            cmp #85             ; Check the range for the VIC-20 frequency
            bcs Main            ;   table. We're allowing note #s 48-85 in
            cmp #48             ;   this simple demo
            bcc Main            ;   ,,
            ;sec                ; Know carry is set from previous cmp
            sbc #48             ; Subtract 48 to get frequency table index
            tax                 ; X is the index in frequency table
            tya                 ; Put the velocity in A
            lsr                 ; Shift 0vvvvvvv -> 00vvvvvv
            lsr                 ;       00vvvvvv -> 000vvvvv
            lsr                 ;       000vvvvv -> 0000vvvv
            bne setvol          ; Make sure it's at least 1
            lda #1              ; ,,
setvol:     sta VOLUME          ; Set volume based on high 4 bits of velocity
            lda FreqTable,x     ; A is the frequency to play
            sta VOICE           ; Play the voice
            jmp Main            ; Back for more MIDI messages
            

; NMI Interrupt Service Routine
; If the interrupt is from a byte from the User Port, add it to the MIDI message
; Otherwise, just go back to the normal NMI (to handle STOP/RESTORE, etc.)
ISR:        pha                 ; NMI does not automatically save registers like
            txa                 ;   IRQ does, so that needs to be done
            pha                 ;   ,,
            tya                 ;   ,,
            pha                 ;   ,,
            jsr CHKMIDI         ; Is this a MIDI-based interrupt?
            bne midi            ;   If so, handle MIDI input
            jmp $feb2           ; Back to normal NMI, after register saves
midi:       jsr MAKEMSG         ; Add the byte to a MIDI message
            jmp $ff56           ; Restore registers and return from interrupt

; Frequency numbers VIC-20
; 135 = Note #48
; Between 48 and 85
FreqTable:  .byte 135,143,147,151,159,163,167,175,179,183,187,191
            .byte 195,199,201,203,207,209,212,215,217,219,221,223
            .byte 225,227,228,229,231,232,233,235,236,237,238,239
            .byte 240,241

#include "midikernal.asm"
Post Reply