Cv/gate/midi
Moderator: Moderators
Re: Cv/gate/midi
@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.
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...
I do not believe in obsolete...
- 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
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.
- 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
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:
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:
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:
But the hooked-up line has high voltage:
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.
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:
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
But the hooked-up line has high voltage:
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.
- 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
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.
The Arduino sketch looks like this:
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.
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.
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;
}
}
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.
- 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
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
And the VIC-20 test program looks like this:
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!
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);
}
}
Code: Select all
10 POKE 37138,255
20 POKE 37148,128
30 INPUT "VALUE (0-15)";V
40 POKE 37136,V
50 GOTO 30
So, all right, this is getting fun!
- 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
Here's the sketch for MIDI:
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:
That's connected to the 5V, ground, and TX pin on the Nano. And here are all the other data pins hooked up:
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:
On the VIC-20, my proof-of-concept was this BASIC program:
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.
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);
}
}
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:
That's connected to the 5V, ground, and TX pin on the Nano. And here are all the other data pins hooked up:
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:
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
(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.
- 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
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.
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.
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
Last edited by chysn on Sat Sep 04, 2021 11:16 pm, edited 1 time in total.
- 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
CV Out and Gate are done. Here's the new interface board:
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:
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:
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.
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.
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:
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;
}
}
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
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.
Re: Cv/gate/midi
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.
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...
I do not believe in obsolete...
- 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
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.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.
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.
- 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
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
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
- 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
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.
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.
- 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
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.
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.
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"
I anticipate having to solve some hardware problems with MIDI In, though. I just hope it's not too slow.
- 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
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.
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"