Propeller 2 version of Auduino granular synthesizer
I am slowly learning to program the Propeller 2 chip and as an exercise I decided to implement the remarkable Auduino granular synth on the P2. Auduino was originally written for the Arduino platform and is capable of producing a far wider range of timbres than might be expected given its relative simple design.
For my P2 implementation, I built a control panel with two analog joysticks and some potentiometers, and I added a MIDI interface. In the video below, a demonstration of how Auduino-style granular synthesis works is followed by some analog noodling, then some MIDI keyboard noodling, and finally a rendition of one of my favorite synth solos.
Here's the code. I would be grateful for any suggestions for improvement.
CON
_xinfreq = 20_000_000
_clkfreq = 288_000_000
BASEPIN_DAC = 6
NPOTS = 7
iDECAY0 = 0
iINC0 = 1
iDECAY1 = 2
iINC1 = 3
iINC = 4
iVOL0 = 5
iVOL1 = 6
dat
potPins byte 8, 9, 10, 11, 13, 12, 14
minValue word 0, 100, 0, 100, 0, 100, 0
maxValue long 200, 4000, 200, 4000, 2000, 1024, 1024
potValues long 0, 0, 0, 0, 0, 0, 0
var
long stack1[40]
long stack2[40]
OBJ
adc[NPOTS]: "jm_analog_in"
midi: "jm_fullduplexserial"
tx: "jm_fullduplexserial"
var
word midiNote
word midiVel
word inc
pri midiTask() | state, b, s, n, l, bend
bend := 0 << 24 ' +/- semitones << 24
midi.start(25, -1, 0, 31_250)
midiNote := -1 ' something invalid
repeat
b := midi.rx()
if b & $80
s := b & $f0 ' ignore channel
state := 0
else
if state == 0 ' note on/off note byte or pitch bend low byte
if s == $90 or s == $80
n := b
state := 1
elseif s == $e0
l := b
state := 2
elseif state == 1 ' note on/off velocity
if s == $90 ' note on
midiNote := n
midiVel := b
inc := incFromNote(midiNote, bend)
else 'note off
if midiNote == n
midiVel := 0
inc := 0
state := 0
elseif state == 2 ' pitch bend high byte
bend := b << 7 + l ' 16-bit bend [0..16384)
bend -= $2000 ' [-8192..8192)
' bend >> 12 would be [-2...2), +/- 2 semitones
' but we need to scale by 2^24, so:
bend <<= 24 - 12
if midiVel
inc := incFromNote(midiNote, bend)
state := 0
pri incFromNote(note, bend) : result | freq, K
{
bend is semitones * 2^24
freq = 2 ^ ((note - 69) / 12) * 440
inc = freq / 48000 * 65536
}
K := 16
freq := QEXP ((((note - 69) << 24 + bend) / 12) << 3 + K << 27) * 440 ' frequency << K
result := freq / 48000
PUB main()
playSounds(false)
pri playSounds(isMidi) | i, ct, y , amp0, decay0, inc0, ph0, amp1, decay1, inc1, ph1, ph, out, vol0, vol1
cogspin(NEWCOG, potTask(), @stack1)
cogspin(NEWCOG, midiTask(), @stack2)
pinstart(BASEPIN_DAC addpins 1, P_DAC_124R_3V | P_DAC_DITHER_RND | P_OE, 6_000, 0) ' 48kHz
wypin(BASEPIN_DAC addpins 1, $8000)
waitms(500)
ct := getct()
repeat
inc0 := potValues[iINC0]
decay0 := potValues[iDECAY0]
inc1 := potValues[iINC1]
decay1 := potValues[iDECAY1]
vol0 := potValues[iVOL0]
vol1 := potValues[iVOL1]
ifnot isMidi
inc := potValues[iINC]
ph += inc
if ph.[16]
ph.[16] := 0
ph0 := 0
amp0 := vol0 << 16
ph1 := 0
amp1 := vol1 << 16
y := ph0.[14..0] ' 0..7fff
if ph0.[15]
!= y
y -= $4000 ' -$4000..3fff
y scas= amp0
out := y
y := ph1.[14..0] ' 0..7fff
if ph1.[15]
!= y
y -= $4000 ' -$4000..3fff
y scas= amp1
out += y
out ^= $8000
waitct(ct += 6000)
wypin(BASEPIN_DAC addpins 1, out)
ph0 += inc0
ph1 += inc1
amp0 sca= $ffff_ffff + (decay0 - 200) << 16
amp1 sca= $ffff_ffff + (decay1 - 200) << 16
pri potTask() | i
repeat i from 0 to NPOTS - 1
adc[i].start(potPins[i], minValue[i], maxValue[i])
repeat
repeat i from 0 to NPOTS - 1
potValues[i] := adc[i].read()
con { license }
{{
Terms of Use: MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be included in all copies
or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
}}
Comments
Neato!
I would suggest moving midiTask() and incFromNote(note, bend) below main() so there is no confusion where the program starts. I've been experimenting with MIDI a bit, but haven't fully sussed out dealing with my keyboard/controller (a 25-yr olf M-Audio Oxygen 8) that sends timing data mixed in with notes, and does running status style outputs).
Consider declaring a constant for your MIDI input pin, as that could move with different designs.
Given the rate of MIDI you could save a cog removing one instance of jm_fullduplexserial and using the smart pin directly.
pub midi_task() | x x := ((clkfreq / 31_250) << 16) | 7 pinstart(MIDI_IN, P_ASYNC_RX, x, 0) repeat if (pinread(MIDI_IN)) b := rdpin(MIDI_IN) >> 24 if b ...
Thank you for the great suggestions, @JonnyMac, and thanks for jm_analog_in and jm_fullduplexserial! Some day I'll understand smart pins.
I don't know how much time you have left in your 48kHz loop, but you might find a few space code cycles here and there by running simple timing tests like this:
t := getct() y := x.[14..0] t := getct()-t-40 debug(udec_long(t)) t := getct() y := x & $7FFF t := getct()-t-40 debug(udec_long(t))
Note that the second version is faster
A few minutes later...
Well, it turns out you have a lot of time in your loop, even if using Propeller Tool (not compiled).
I added pinlow() immediately before your waitct() and pinhigh() immediately after. You've got ~10us of available bandwidth.
nice
Thanks, @Ludis !