Java Synthesizer, Part 17, Cleanup 2


If you’re a professional programmer, odds are that most of your code is coming out of a CAD system, so you don’t really need to write many lines from scratch. Even if you’re not using CAD, you still probably have a significant design phase where you do all your planning on paper first, and then the coding is just a matter of transcribing from the plan to the development environment. You don’t need to worry much about what you’re doing during the actual typing phase because everything’s already been laid out for you in advance.  Contrast this with the hobbyist, who is experimenting as they go along, learning with each mistake and spending hours on debug because they forgot something obvious.  The thing to remember about hobbyists is that most of what they create is for themselves and is never intended for a commercial market.  They don’t need the rigid rules and design requirements that “professionals” lean on. “Good enough” really is good enough.

I say this because I’ve been ripped by “professional” engineers accusing me of not knowing all the specs for something I’m working on, when what I really want is for someone to answer a question regarding implementation that they apparently themselves aren’t able to answer.  Be that as it may, I get the feeling that I’m trying stuff in Java that’s not really well documented (outside of some overpriced commercial training course), and that means I occasionally find myself rewriting different sections as I go along, without outside help. I either end up adding functionality that I hadn’t thought I’d need previously, or because I finally found a better way to get the job done.  Such is the case now.

I’m slowly working my way towards the user-programmable synth interface I’d mentioned before, and I’m trying to get the various existing modules ready for the Java equivalent of “pointers to a function”. I’m also implementing each of the earlier ideas I’d had for modules and module operating modes (such as attack2, sustain2 and invert for the ADSR). I’m now at a resting point before tackling a user-input script parser (I don’t know if I want to address a graphics-based schematic patch approach or not. Seems like overkill for my needs.)

So, what’s changed recently?

First, I turned the A-300 Pro MIDI parser section into an action listener. It doesn’t seem to have made any impact on the clicking from the sound engine, but it’s the first step to putting addBuffer() into its own listener, and it removes the workload on the timer method.

Second, I focused more on the circuit object. This is the object that contains pointers to each of the modules for determining which data entry screens I’m going to get for the various modules so I can adjust settings using the jSliders on the screen (or the MIDI controls on the A-300). Each new item in circuit represents a separate module (osc1, osc2, etc.) and a separate data entry screen. There’s one “main screen”, screen 0, where I can change the settings for the most important module items all in one place (i.e. – circuit volume, osc1 frequency and waveform and the ADSR attack, decay, sustain and release). circuit will be the starting point for the user wiring script parser.

Example:

k = 0;
circuits.add(new objectNamePair(osc1, “Main Panel”));
circuits.get(k).ranges.add(new range(0, 16000, 512, 256,    “Volume”,        “d”));
circuits.get(k).ranges.add(new range(50, 1000, 512, 256,    “Osc1 Freq.”,    “d”));
circuits.get(k).ranges.add(new range(0,     4,   4,   0,    “Osc1 Waveform”, “i”));
circuits.get(k).ranges.add(new range(0.1,  20, 512, 256,    “Osc2 Freq.”,    “d”));
circuits.get(k).ranges.add(new range(0.0, 1.0, 100,  49,    “Osc2 Ratio”,    “d”));
circuits.get(k).ranges.add(new range(-2.0,  2.0, 400, 199,  “Bender Offset”, “d”));
circuits.get(k).ranges.add(new range(0,    512, 512, 512,   “Filter”,    “d”));
circuits.get(k).ranges.add(new range(0, 8000, 512, 255,     “Attack”,  “d”));
circuits.get(k).ranges.add(new range(0, 8000, 512, 255,     “Decay”,   “d”));
circuits.get(k).ranges.add(new range(0,  1.0, 400, 200,     “Sustain”, “d”));
circuits.get(0k).ranges.add(new range(0, 8000, 512, 255,     “Release”, “d”));
int k++;
circuits.add(new objectNamePair(osc1, “osc1”));
circuits.get(k).ranges.add(new range(50, 1000, 512, 256, “Freq.”,            “d”));
circuits.get(k).ranges.add(new range(0,     4,   4,   0, “Waveform”,         “i”));
circuits.get(k).ranges.add(new range(0.0, 1.0, 100,  49, “Ratio”,            “d”));
circuits.get(k).ranges.add(new range(0,    50,  50,   0, “Glide Smoothness”, “i”));
circuits.get(k).ranges.add(new range(0,    50,  50,   0, “Glide Width”,      “i”));
circuits.get(k).ranges.add(new range(0,     1,   1,   0, “Enable Glide”,     “b”));
circuits.get(k).ranges.add(new range(0,     1,   1,   0, “Gate”,             “b”));
k++;

Third, I added a “module” class that all the other modules inherit from, for standardizing calls to .setStr(), .strVal(), .toggle() and .toggleGlide() for the osc, vca, adsr, etc. classes. Which means I also had to add those methods to each of the other classes.

abstract class module {
abstract void   setStr(int idx, String str);
abstract String strVal(int idx);
abstract void   toggle();
abstract void   toggleGlide();
}

=========

class osc extends module {

@Override void toggle() {
gate = (! gate);
}
@Override void toggleGlide() {
enableGlide = (! enableGlide);
}
@Override String strVal(int idx) {
String ret = “Invalid variable”;
switch (idx) {
case 0: ret = Double.toString(freq);
break;
case 1: ret = Integer.toString(waveform);
break;
case 2: ret = Double.toString(ratio);
break;
case 3: ret = Integer.toString(glideSmoothness);
break;
case 4: ret = Integer.toString(glideWidth);
break;
case 5: ret = Boolean.toString(enableGlide);
break;
case 6: ret = Boolean.toString(gate);
break;
}
return(ret);
}
@Override void setStr(int idx, String str) {
if(! str.trim().isEmpty()) {
switch (idx) {
case 0: setFreq(Double.parseDouble(str));
break;
case 1: waveform        = (int) Double.parseDouble(str);
break;
case 2: ratio           = Double.parseDouble(str);
break;
case 3: glideSmoothness = (int) Double.parseDouble(str);
break;
case 4: glideWidth      = (int) Double.parseDouble(str);
break;
case 5: enableGlide     = str2bool(str);
break;
case 6: gate            = str2bool(str);
break;
}
}
}
}
Fourth, I decided to embed a VCA module in the noise module. The reason for this is that noise is one of those features where you really don’t want it at 100% volume. Ever. So, since I’d be dedicating a VCA to attenuating the noise output anyway, it might as well be within the noise class itself. Another benefit is that it simplifies the circuit wiring section later. (The pan class also has its own vca.)

Fifth, as mentioned above, I went ahead and added attack2 and sustain2 to the ADSR, which activate when the user lets go of the keyboard key, prior to going into the release phase. As I started adding the logic for what to do if attack2 is 0, I realized that my earlier approach of tracking each phase to avoid introducing dead cycles and causing clicking was just giving myself more work. It’s easier to just use a “while(! done)” loop with a switch-case to walk through each ADSR phase with 0 length and stop at the beginning of the first non-zero phase (e.g., if attack, pitch and decay are 0, start at sustain with a count of 0, and set done = true). Now, I can very easily add a decay2 phase if I want and I don’t have to worry about remembering what my intended logic was. Anyway, I ripped out the old code and replaced it with the “while(! done)” loop. I followed this up with an invert toggle, which turns the sound envelope upside-down.

Sixth, I moved the VCF ifs and for-loops that had been in addBuffer() into the vfc class.  Again, this is for simplifying the addBuffer() code when I get to user-programmable synth circuits. I also wanted to add some new filtering techniques, so there are 2 new modes. One decays the FFT frequency bins based on magnitude thresholds (if magnitude < 50,000, decay that bin by 80%), and the second combines the existing frequency filtering with the new mode 1. Because some of the magnitudes can get really big, I converted the magnitude threshold check to log base-10. So, the threshold range is just 0 to 7.0.

Seventh brings me to the first really new piece of code – the mixer module. Unlike the Java system mixer (which I unsuccessfully played with in an earlier blog entry), this mixer is designed specifically to ease the synth design. There are certain places in the synth where two or more signals go to the same point, examples being keyBoard.gateOut and osc2.gateOut driving adsr1.gateIn; and osc1, echo1 and noise1 all going to adsr.dataIn. Rather than trying to figure how to softcode this from the GUI, I created the mixer class to take multiple inputs, average them and run them through a VCA before returning them from mixer.out.

(Example circuit showing audio-type mixer at ADSR input.)

One of the drawbacks to simply averaging signals is that you get amplitude loss. Say you want osc1 to output a sinewave from +/-1, the noise generator outputting +/- 0.4, and echo1 feeding back the signal at +/- 0.6. The final total signal will be +/- 0.66. If the ADSR outputs to vca1 with a maximum volume of 16,000 (when the input is 1.0), then the total averaged signal going to the sound engine will be 10,666, instead of the originally desired 16,000, and it sounds weaker. I could boost vca for a range from 0 to 24,000, but I’d be altering vca1’s range with every new circuit patch. It makes more sense to put a dedicated vca inside the mixer class to compensate for averaging regardless of the number of pins involved.

The mixer class also handles booleans for gate signals (I just have an AND function right now); and multiplying signals together for scaling the ADSR output with the pan waveform.

The last real change is to addBuffer(), which is starting to look like it contains the output from a code generator. This is an intermediary step in the process of turning everything into “pointers to functions”.

private void addBuffer() {
ByteBuffer    byteBuffer;
ShortBuffer   shortBuffer;
byteBuffer  = ByteBuffer.wrap(audioBuffer[audioBufferPtr]);
shortBuffer = byteBuffer.asShortBuffer();

// Temp variable to store calculation results
double [] hold    = new double[AUDIO_SLICE_SHORT];

osc1.pitchBend = keyBoard.pitchWheelOut();
// Harmonic oscillator 3 relative to oscillator 1’s frequency
osc3.setFreq(bender1.out(osc1.getFreq()));

for(int pCnt = 0; pCnt < AUDIO_SLICE_SHORT; pCnt++){
osc2.nextSlice();  // Increment oscillator 2

mix1.in(0, keyBoard.gateOut());
mix1.in(1, osc2.gateOut());
adsr1.gateIn(mix1.outAndBool());  // Turn on ADSR via the keyboard or an oscillator

noise1.gateIn(adsr1.attackEvent());

mix2.in(0, osc1.nextSlice());
mix2.in(1, noise1.addNoise());
mix2.in(2, echo1.out());

hold[pCnt] = adsr1.nextSlice(mix2.out());
echo1.in(hold[pCnt]);
pan1.buildPan(pCnt);
vcf1.dataIn(pCnt, new Complex(hold[pCnt], 0));  // Add data to filter buffer
}

hold = vcf1.applyFilter(hold); // Do the actual FFT filtering
}

Raw formatted textfile of full app here.

 

Advertisements
Leave a comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: