Shop OBEX P1 Docs P2 Docs Learn Events
E-Guitar synthesizer in 50 lines of Spin 2 — Parallax Forums

E-Guitar synthesizer in 50 lines of Spin 2

Wuerfel_21Wuerfel_21 Posts: 5,530
edited 2024-05-09 15:31 in Propeller 2
CON
SAMPLE_RATE = 44_100
_CLKFREQ = SAMPLE_RATE * 256 * 24
AUDIO_LEFT = 24+6 ' this pin and the next one will have audio
MAX_PERIOD = 16_000
VAR
long string_length
long buffer_ptr
long exciter_on
long retrigger_ctr
long note_cnt
word string_buffer[MAX_PERIOD]
PUB main() | sample, filter,seed, lpf, hpf
pinstart(AUDIO_LEFT addpins 1,P_DAC_124R_3V|P_DAC_DITHER_PWM|P_OE,CLKFREQ/SAMPLE_RATE,$7F80)
repeat
    repeat until pinr(AUDIO_LEFT) ' wait for DAC ready
    wypin(AUDIO_LEFT addpins 1,(sample sar 4)+$7F80) ' set sample
    if --retrigger_ctr < 0 ' trigger new notes
        retrigger_ctr := SAMPLE_RATE /4 ' time to next note
        string_length := (SAMPLE_RATE / getnote()) <# MAX_PERIOD ' tune string
        buffer_ptr := 0
        exciter_on := 1
    if exciter_on
        sample := getrnd() signx 15 ' during first period, generate noise
    else
        sample := (string_buffer[buffer_ptr] signx 15) ' afterwards, read from buffer
    sample := (sample + filter) sar 1 ' incrementally filter sample
    filter := sample
    sample := clamp(sample)
    string_buffer[buffer_ptr] := sample
    if ++buffer_ptr > string_length ' wrap buffer around
        exciter_on := buffer_ptr := 0 ' also turn exciter off
    if 1 ' E-Guitar amplifier simulation - disable for very dry sound
        sample *= 3 ' gain knob
        hpf += (sample-=hpf) sar 3 ' pre-amp AC coupling
        sample := waveshape(sample)
        lpf += (sample-lpf) sar 2 ' just an LPF to make it less harsh
        sample := lpf
PRI getnote() : r
    case (note_cnt+/12)+//3 ' simple arpeggio
        0: r := lookupz(note_cnt+//3 : 262, 311, 392) ' C4 - Eb4 - G4
        1: r := lookupz(note_cnt+//3 : 207, 262, 311) ' A3 - C4 - Eb4
        2: r := lookupz(note_cnt+//3 : 233, 294, 349) ' Bb3 - D4 - F4
    r >>= 1 ' transpose one octave down
    note_cnt++
PRI waveshape(x) : r ' wave shaping using arctan transfer function
    _,r := xypol($2000,x) 
    return clamp(r sar 15)
PRI clamp(x) : r ' clamp to valid 16 bit range
    return x <# $7FFF #> -$7FFF

Something I wrote to pass an hour or so. It's a basic Karplus-Strong synth with a rudimentary tube amp simulation. It'd probably sound better if there were multiple channels and the decay of the previous note was allowed to continue. Or to play chords, of course.

Comments

  • RaymanRayman Posts: 15,587

    This looks interesting. Is that complete? Looks like it needs some note data somewhere to play something?

    Seems could be warped into MIDI player...

  • @Rayman said:
    This looks interesting. Is that complete? Looks like it needs some note data somewhere to play something?

    Seems could be warped into MIDI player...

    Yea, it just plays a little sequence of notes

  • @Wuerfel_21 said:

    CON
    SAMPLE_RATE = 44_100
    _CLKFREQ = SAMPLE_RATE * 256 * 24
    AUDIO_LEFT = 24+6 ' this pin and the next one will have audio
    MAX_PERIOD = 16_000
    VAR
    long string_length
    long buffer_ptr
    long exciter_on
    long retrigger_ctr
    long note_cnt
    word string_buffer[MAX_PERIOD]
    PUB main() | sample, filter,seed, lpf, hpf
    pinstart(AUDIO_LEFT addpins 1,P_DAC_124R_3V|P_DAC_DITHER_PWM|P_OE,CLKFREQ/SAMPLE_RATE,$7F80)
    repeat
        repeat until pinr(AUDIO_LEFT) ' wait for DAC ready
        wypin(AUDIO_LEFT addpins 1,(sample sar 4)+$7F80) ' set sample
        if --retrigger_ctr < 0 ' trigger new notes
            retrigger_ctr := SAMPLE_RATE /4 ' time to next note
            string_length := (SAMPLE_RATE / getnote()) <# MAX_PERIOD ' tune string
            buffer_ptr := 0
            exciter_on := 1
        if exciter_on
            sample := getrnd() signx 15 ' during first period, generate noise
        else
            sample := (string_buffer[buffer_ptr] signx 15) ' afterwards, read from buffer
        sample := (sample + filter) sar 1 ' incrementally filter sample
        filter := sample
        sample := clamp(sample)
        string_buffer[buffer_ptr] := sample
        if ++buffer_ptr > string_length ' wrap buffer around
            exciter_on := buffer_ptr := 0 ' also turn exciter off
        if 1 ' E-Guitar amplifier simulation - disable for very dry sound
            sample *= 3 ' gain knob
            hpf += (sample-=hpf) sar 3 ' pre-amp AC coupling
            sample := waveshape(sample)
            lpf += (sample-lpf) sar 2 ' just an LPF to make it less harsh
            sample := lpf
    PRI getnote() : r
        case (note_cnt+/12)+//3 ' simple arpeggio
            0: r := lookupz(note_cnt+//3 : 262, 311, 392) ' C4 - Eb4 - G4
            1: r := lookupz(note_cnt+//3 : 207, 262, 311) ' A3 - C4 - Eb4
            2: r := lookupz(note_cnt+//3 : 233, 294, 349) ' Bb3 - D4 - F4
        r >>= 1 ' transpose one octave down
        note_cnt++
    PRI waveshape(x) : r ' wave shaping using arctan transfer function
        _,r := xypol($2000,x) 
        return clamp(r sar 15)
    PRI clamp(x) : r ' clamp to valid 16 bit range
        return x <# $7FFF #> -$7FFF
    

    Something I wrote to pass an hour or so. It's a basic Karplus-Strong synth with a rudimentary tube amp simulation. It'd probably sound better if there were multiple channels and the decay of the previous note was allowed to continue. Or to play chords, of course.

    Nice!
    Once again cordic shines!
    I am a great fan of asymmetric overdrive. So perhaps you might want to add some offset before the arctan, which will add 2nd harmonics and therefore enrich the sound in a nice way.
    Also a little bit of even a very simple reverb can do much....
    Christof

  • Wuerfel_21Wuerfel_21 Posts: 5,530
    edited 2025-09-08 18:00

    I dug this out again and played with it some more.
    I may have went overboard.

    Compile as flexspin -2 karplus2.spin2 --compress - comes out to less than 4K of code!
    Pin config is also at the top of karplus2.spin2 (defaults to pins 54 and 55)

    What this actually does is left as a suprise.

    EDIT: added variant with slightly less annoying volume balance

  • @Wuerfel_21 said:
    I dug this out again and played with it some more.
    I may have went overboard.

    lol understatement compared to the OP - cool though... I would've expected a lot more code, or PASM, but yeah this is really small (seems like most of it is the music data itself). I'm definitely not a musically inclined person but I've started to really get interested in synths, midi, etc and have (mostly unsuccessfully) tried muddling through making little bits of code related to each, so it's neat to take a look at the guts of something like this actually working.

    Cheers

  • @avsa242 said:

    @Wuerfel_21 said:
    I dug this out again and played with it some more.
    I may have went overboard.

    lol understatement compared to the OP - cool though... I would've expected a lot more code, or PASM, but yeah this is really small

    All ad-hoc code I just kinda threw together over the course of a (far too long) evening. If I was doing a proper synth object, it would probably be ASM and a little bit less hard-coded. OTOH it's fun to change those hardcoded values and add/remove code and listening to result.

    (seems like most of it is the music data itself). I'm definitely not a musically inclined person but I've started to really get interested in synths, midi, etc and have (mostly unsuccessfully) tried muddling through making little bits of code related to each, so it's neat to take a look at the guts of something like this actually working.

    Feel free to steal any of the code from this. Sorry there aren't more comments. The 0..127 note numbers are MIDI-spec and the routines to convert those into periods and NCO frequencies are probably useful to copy into any sort of music project.

Sign In or Register to comment.