Shop OBEX P1 Docs P2 Docs Learn Events
Propeller 2 version of Auduino granular synthesizer — Parallax Forums

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

  • JonnyMacJonnyMac Posts: 9,141
    edited 2023-05-21 14:29

    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 ...
    
  • mparkmpark Posts: 1,305

    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

  • JonnyMacJonnyMac Posts: 9,141
    edited 2023-05-22 04:07

    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

  • mparkmpark Posts: 1,305

    Thanks, @Ludis !

Sign In or Register to comment.