Created
March 3, 2023 06:23
-
-
Save awonak/1e282b9cf61400a8643bcff8f2453cbc to your computer and use it in GitHub Desktop.
You really think someone would do that, just turn the Uncertainty into a quantized digital oscillator?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"log" | |
"machine" | |
"math" | |
"time" | |
"tinygo.org/x/drivers/tone" | |
) | |
const ( | |
// GPIO mapping to Uncertainty panel. | |
CVInput = machine.ADC0 | |
CV1 = machine.GPIO27 | |
CV2 = machine.GPIO28 | |
CV3 = machine.GPIO29 | |
CV4 = machine.GPIO0 | |
CV5 = machine.GPIO3 | |
CV6 = machine.GPIO4 | |
CV7 = machine.GPIO2 | |
CV8 = machine.GPIO1 | |
// Number of times to read analog input for an average reading. | |
ReadSamples = 500 | |
// Calibrated average min read uint16 voltage within a 0-5v range. | |
MinCalibratedRead = 415 | |
// Calibrated average max read uint16 voltage within a 0-5v range. | |
MaxCalibratedRead = 29582 | |
// Upper limit of voltage read by the cv input. | |
MaxReadVoltage float64 = 5 | |
// The first midi note number for 0v. C1 | |
MinNoteNum = 24 | |
// The max midi note number from a range of 12 notes per octave * 5 octaves + root note 24. | |
MaxNoteNum = 84 | |
// Enable to print serial monitoring log messages. | |
Debug = true | |
) | |
var ( | |
// Create package global variables for the cv input and outputs. | |
cvInput machine.ADC | |
cvOutputs [8]machine.Pin | |
// We need a rather high frequency to achieve a stable cv ouput, which means we need a rather low duty cycle period. | |
// Set a period of 500ns. | |
defaultPeriod uint64 = 1e9 / 500 | |
) | |
type VCO struct { | |
speaker tone.Speaker | |
scale Scale | |
currentNote tone.Note | |
} | |
func NewVCO(pwm tone.PWM, pin machine.Pin, scale Scale) VCO { | |
err := pwm.Configure(machine.PWMConfig{ | |
Period: defaultPeriod, | |
}) | |
if err != nil { | |
log.Fatal("pwm Configure error: ", err.Error()) | |
} | |
speaker, err := tone.New(pwm, pin) | |
if err != nil { | |
log.Fatalf("NewVCO(%v) error: %v", pin, err.Error()) | |
} | |
return VCO{ | |
speaker: speaker, | |
scale: scale, | |
currentNote: tone.Note(0), | |
} | |
} | |
func (vco *VCO) SendNote(note tone.Note) { | |
if note == vco.currentNote { | |
return | |
} | |
// Check if new note is in the quantized scale. If so, set the note. | |
for _, n := range vco.scale { | |
if note == n { | |
vco.speaker.SetNote(note) | |
vco.currentNote = note | |
} | |
} | |
} | |
type Scale []tone.Note | |
func NewScale(steps []int) Scale { | |
var ( | |
scale Scale | |
note int = MinNoteNum | |
step int = 1 | |
) | |
// If there are no steps provided in the param, there's no work to be done. | |
if len(steps) == 0 { | |
return scale | |
} | |
// Iterate over all midi note numbers in our range to determine which notes belong in this scale. | |
for note < MaxNoteNum { | |
// Check if the current note's step within an octave is present in the notes parameter. | |
for _, s := range steps { | |
if step == s { | |
scale = append(scale, tone.Note(note)) | |
break | |
} | |
} | |
// Increment the step index within an octave range, resetting at 12 steps. | |
step += 1 | |
if step > 12 { | |
step = 1 | |
} | |
// Increment to the next note number. | |
note += 1 | |
} | |
return scale | |
} | |
func readCV() int { | |
// Read the cv input clipped to a 0-5v range. | |
var sum int | |
for i := 0; i < ReadSamples; i++ { | |
read := int(cvInput.Get()) - math.MaxInt16 | |
if read < 0 { | |
read = 0 | |
} | |
sum += read | |
} | |
return sum / ReadSamples | |
} | |
func readVoltage() float64 { | |
read := readCV() | |
return MaxReadVoltage * (float64(read-MinCalibratedRead) / float64(MaxCalibratedRead-MinCalibratedRead)) | |
} | |
// Get the midi note number from a range of 60 notes (12 notes per octave * 5 octaves), starting at note number 24 (C1). | |
func noteFromVoltage(v float64) tone.Note { | |
noteNum := int(v/MaxReadVoltage*MaxNoteNum) + MinNoteNum | |
return tone.Note(noteNum) | |
} | |
func init() { | |
// Initialize the cv input GPIO as an analog input. | |
machine.InitADC() | |
cvInput = machine.ADC{Pin: CVInput} | |
cvInput.Configure(machine.ADCConfig{}) | |
// Create an array of our cv outputs and configure for output. | |
cvOutputs = [8]machine.Pin{CV1, CV2, CV3, CV4, CV5, CV6, CV7, CV8} | |
for _, cv := range cvOutputs { | |
cv.Configure(machine.PinConfig{Mode: machine.PinOutput}) | |
} | |
} | |
func main() { | |
if Debug { | |
// Provide a brief pause to allow time to start up the serial monitor to capture errors. | |
log.Print("START...") | |
time.Sleep(time.Second * 5) | |
log.Print("Ready...") | |
} | |
// Define a few fun scales. | |
chromatic := NewScale([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) | |
minorTriad := NewScale([]int{1, 4, 8}) | |
majorPentatonic := NewScale([]int{1, 3, 5, 6, 8}) | |
octave := NewScale([]int{1, 12}) | |
// Initialize a collection of PWM VCOs bound to a scale. | |
vcos := []VCO{ | |
NewVCO(machine.PWM6, cvOutputs[1], chromatic), | |
NewVCO(machine.PWM0, cvOutputs[3], minorTriad), | |
NewVCO(machine.PWM2, cvOutputs[5], majorPentatonic), | |
NewVCO(machine.PWM0, cvOutputs[7], octave), | |
} | |
// Main program loop. | |
for { | |
newNote := noteFromVoltage(readVoltage()) | |
for _, vco := range vcos { | |
vco.SendNote(newNote) | |
} | |
if Debug { | |
log.Printf("readCV: %d\tvoltage: %f\tnote: %v\n", readCV(), readVoltage(), newNote) | |
time.Sleep(time.Millisecond * 10) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment