πano -- A physically modelled (real-time) plucked-string synth


I've put together a πano here:


It's based closely on http://pipad.org/src/spiral/Jaffe-Smith-Extensions-CMJ-1983.pdf -- in fact the variables and expressions in my filters.h file match the equations in the paper, although I've tidied up the maths in a couple of places.

Actually this is a rework of a JavaScript project: http://pipad.org/src/spiral/spiral.html specifically http://pipad.org/src/spiral/karplus.js <-- the JS code here is cleaner as I am using Greek symbols for variable names, and also JS allows for an easier-to-read style in this particular situation.  (NOTE: If you click the notes at the edges it corresponds to a softer pluck. The timbre varies with the velocity). (NOTE: amazingly visual studio compiled with Greek symbol variable names. But then it fell over half an hour later. So I bit the bullet and did some search-and-replace.)

It performs perfectly well on my 3.1GHz iMac, if anyone would care to test drive it on a mobile device I would be interested to know whether it hacks it.

Critique / feedback / even code contributions & collaboration most welcome as always. I know my AudioSettings.h is a bit of a mess, I lifted it from another project.

I'm not deriving from AudioAppComponent as (1) I like to do it manually as an exercise, (2) I want to keep the possibility open for handling microphone input separately.

Also I'm not sure what the best strategy is for putting a JUCE project up on GitHub ... I'm including the whole of JUCE, which is rather inelegant. Also Visual Studio project gubbins. Ideally I think I would just have my own source files, the ProJucer file, and maybe some cunning way of forcing the JUCE files to download... would be quite nice if the ProJucer could remember the location of them independent of the project, even keep them updated...

If anyone is interested in using it, I can help out on IRC...


PS I could optimise my filter code some, but I don't think I can process it entirely in blocks as it involves a feedback loop.



Just an FYI: I've no idea if this is intentional, but you've plenty of tabs (mixed with spaces) in the code.


Hmm, in filters.h:JOS.renderToBuffer:

env *= powf( env_decay, (float)samples);
energy *= powf(alpha*env_decay, (float)samples);

You're only updating the envelope once per block. Wouldn't it be better to at least update it at a fixed rate (right now it could be updated every 1024 samples, or every 10)?


You don't even need the powf, you can just do env *= env_decay; inside the loop. Same with energy. Then you get sample-accurately updated envelopes, virtually for free.


e: also, not sure how the performance is, but using virtual calls for everything per sample is a textbook example of what not to do. There's actually no reason to do (you don't need polymorphism, everything is statically typed out - use CRTP if needed). No reason to create your filters using new() either, you're probably hurting cache locality.


Stirling effort, but I have some build problems (MS VS2013)

Warning    2    warning C4512: 'JOS' : assignment operator could not be generated    c:\...\devel\piano\source\Filters.h    272    1    Piano
Warning    1    warning C4189: 'd' : local variable is initialized but not referenced    c:\...\devel\piano\source\AudioSettings.h    84    1    Piano
    10    IntelliSense: identifier "SynthPlayer" is undefined    c:\Users\Andrew Cordani\Documents\Devel\Piano\Source\Synth.h    76    2    Piano
Error    4    error C4430: missing type specifier - int assumed. Note: C++ does not support default-int    c:\...\devel\piano\source\Synth.h    76    1    Piano
Error    7    error C2228: left of '.setSource' must have class/struct/union    c:\...\devel\piano\source\Synth.h    90    1    Piano
Error    9    error C2228: left of '.setSource' must have class/struct/union    c:\...\devel\piano\source\Synth.h    100    1    Piano
Error    3    error C2146: syntax error : missing ';' before identifier 'audioSourcePlayer'    c:\...\devel\piano\source\Synth.h    76    1    Piano
Error    5    error C2065: 'audioSourcePlayer' : undeclared identifier    c:\...\devel\piano\source\Synth.h    87    1    Piano
Error    6    error C2065: 'audioSourcePlayer' : undeclared identifier    c:\...\devel\piano\source\Synth.h    90    1    Piano
Error    8    error C2065: 'audioSourcePlayer' : undeclared identifier    c:\...\devel\piano\source\Synth.h    100    1    Piano



Thanks for the feedback. I've just removed and recreated the repository, done it clean this time! So the repo only contains my source files: No build gubbins, no JUCE source.

On my file system in my /Dev I have /JUCE-master and /Piano.

So if you download JUCE latest and Piano from GitHub so they sit next to one another, Piano/Piano.jucer should be able to find JUCE-master, because it is using a relative path ../JUCE-master

readme.MD contains setup instructions.

@jrlanglois, thanks! I have since turned whitespace to visible in VS and converted everything to tabs.

@mayae, good spots!

  • I was initially scaling the envelope each sample, and then figured it would be slightly more efficient to block scale. But now I have reverted back to the original -- it's cleaner and 100 vs 102 CPU cycles per sample -- who cares?! 
  • Really good point about using virtual functions. I will fix that now. (EDIT: fixed!)
  • Regarding new, I was actually leaking! Fixed that with ScopedPointer-s. I can't see how I can cleanly avoid new -- if I don't use pointers, my filter objects get default initialised at the time of object creation, and then re-initialised during JOS's constructor. Using pointers + new avoids this unnecessary default initialisation.

@dub, woops that was a bug due to me using an old JUCE version.  Fixed now!



They will not be default-initialized if you don't have a constructor (or have a constructor that does nothing). Even if, this is many many orders of magnitude faster than a new() call, which btw must never happen (!!!) in processing code. Your voices should be cached, anyway, and not created on each note down! There's a reason virtually all synths have a maximum number of voices, unless they have a custom memory allocator for the audio thread. 


The solution is to have a reset() function, that actually (re-)initializes the filter/voice objects. This is very common anyway, as filters are often sweepable/can change other parameters runtime. Plus you want to support any samplerate changes etc., and have the ability to alter the timbre of a voice on parameter changes (otherwise it gets really annoying to set up a synth as you can only hear changes after playing a new tone).


How does this compare to the Juce Karplus-Strong-based plucked string example?


It gives a more accurate modelling.

https://github.com/julianstorer/JUCE/blob/master/examples/PluckedStringsDemo/Source/StringSynthesiser.h#L73 is where the magic happens in the JUCE example, which corresponds to the first filter diagram in the PDF (fig 1).

This version corresponds to the last one (fig 7).

The section headers in the PDF detail the issue each filter addresses.

The important ones are:

  •  getting exact pitch -- addressing quantisation fail
  •  decay stretching so that high notes don't decay too fast nor low notes too slow
  •  realistic varying decay of harmonics
  •  brightening timbre in proportion to velocity.


PS Those are the only four I've implemented. (optimal trade-off?).


I've just run into the same problem when I tried changing my /JUCE path:


Edit: SOLVED! I must have been using an older version of JUCE. SynthPlayer has been renamed to AudioSourcePlayer at some point.



Nice!  BTW  still a little buglet for novices to resolve.

Error    1    error C2059: syntax error : '}'    c:\devel\piano\source\audiosettings.h    84    1    Piano
Error    2    error C1903: unable to recover from previous error(s); stopping compilation    c:\devel\piano\source\audiosettings.h    88    1    Piano

Easy to resolve by commenting both lines.



Very nice work here. I can't understand the maths of the paper you linked, but I can understand your code! Very interesting to see how the decay stretching works.