Skip to content

Instantly share code, notes, and snippets.

@awonak
Created March 3, 2023 06:23
Show Gist options
  • Save awonak/1e282b9cf61400a8643bcff8f2453cbc to your computer and use it in GitHub Desktop.
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?
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