Bending with Dojo part 2: Stretching in the Dojo

Following on from Part 1, I wanted to make an interactive way to explore the mechanics of string bending. A spreadsheet is the simplest way to do that, but it needs users to download a file and open it up with the right program. A web-app only needs a browser and internet access to use, but takes a bit more assembly. To cut down the amount of DIY needed it's common to use a framework. JQuery is probably the most commonly used, but without built-in support for plots, so I went for the slight more complex Dojo.
I tried to make the maths background in Part 1 understandable for a general audience, but this Dojo section is going to have to assume you know a few programming terms. If that doesn't sound like your thing, then maybe you're just looking for the string bend calculator.


Lying

Not being a frequent Javascript user and using Dojo for the first time I of course prototyped this as a completely procedural program and then started to refactor it into a better design. I'm going to pretend the final version is how it was meant to be all along. However there are some beginner Javascript mistakes (or Dojo peculiarities) I made along the way and they're included. They're useful for me if no-one else.

Aiiieeee (IE)

I'm not anti-Microsoft, but they don't make it easy.  This one had me tearing hair out and tidying HTML and Javascript until I found out it that using Dojo from a CDN (content delivery network) just doesn't work with Dojo 1.9. Finding out that loading Dojo was the problem wasn't made any easier by IE 9 not reporting the failed load in web designer mode, IE 10 does tell you about the problem. Changing to Dojo 1.9.1 fixed that.

What's that in old money?

Guitar related measures (scale, string gauge) are often given in imperial units, so scale is 24.5 inches rather than 648mm and string gauges are given in thousandths of an inch. Metric on the other hand is much simpler for conversions and calculations. So I wanted to have two controls for each of these that would allow you to adjust in either system and update both boxes.
This turns out to be tricky, you set each control to update the other when updated. Now when you update the metric control it updates the imperial control, which wants to update the metric control again, which... etc. While the cycle does stop it leads to weird effects like not being able to set certain metric lengths. Dojo should let you pass a parameter to stop this propagation, but it doesn't work in this situation. The simplest solution is that each box checks whether the other box has focus first and doesn't try to update it if it does.
Incidentally, an inch is now defined to be exactly 25.4mm, so perfect conversion is possible.

Listing nothing

I've made use of some dropdown boxes to provide suggested values for things like density and scale. These are filled from lists provided in the code, mainly of "value" and "name" pairs. Dojo handles null, 0 and null string values the same, which means that even if you need a list of numbers it's best to supply the values as strings, particularly to avoid problems with 0 being interpreted as a default value ("0" instead is okay). That grates a bit on someone more used to strongly type languages, but it's necessary with Javascript.
Picking lint
The code got quite a few passes through the useful JSLint, particularly while trying to fix the IE problem above. Some things that are Javascript idioms which you don't do in other C-like languages became apparent:
  • Variables in a scope get declared with a single 'var' statement, separating multiple declarations and assignments with a "," rather than making multiple ";" ended statements. This includes defining function names.
  • Being statements, anonymous function declarations should have a ";" after the braces (unless the assignment is in a list as above, where a "," might be used).
  • Generally linting javascript is very useful since you don't have the same quality of compiler or interpreter warnings available.

Ready: get, set, call

Because there are a lot of variables in the model for string bending that need to be exposed to the interface and update each other in complex ways a lot of getters and setters and callbacks are needed. Writing them all is hard to maintain and makes the JS bulky, there may be other ways to do this, but the solution I came up with is to place this in the class constructor for the guitar string after a list of "this.property=" assignments:

         var nullfn = function(){},
            ii, key,
            updProps = [ {id: "bendstretch", tgt: "bendlat"},
                         {id: "bendlat", tgt: "bendlatf"},
                         {id: "bendlatf", tgt: null} ];
            this.buildSet = function (key) {
              this["set_"+key] = function ( newval ) {
                this[key]= newval;
                this["updated_" + key]();
              };
              this["cb_updated_"+key] = nullfn;
            };
            this.buildUpdRecalc = function (id, tgt) {
              this["updated_"+id] = function () {
                this["recalc_"+tgt]();
                this["cb_updated_"+id]();
              };
            };
            this.buildUpdOnly = function (id) {
              this["updated_"+id] = function () {
                this["cb_updated_"+id]();
              };
            };
            for (ii=0; ii < updProps.length; ii++ ) {
              key = updProps[ii];
              this.buildSet(key.id);
              if (key.tgt) {
                this.buildUpdRecalc(key.id, key.tgt);
              } else {
                this.buildUpdOnly(key.id);
              }
            }
(Formatting courtesy of hilite.me)
Only a few of the properties are shown, but each one simply defines the next property that should be updated. The loop at the end creates a setter and a null callback for each property. When set a property calls a recalculate function for the next property in the chain (if there is one) and the callback function which can be set by the interface. The buildSet, buildUpdRecalc and buildUpdOnly methods are used to add these functions to the class, using the "this[name]" method to address the class properties (variables or methods) by name. Since they're methods, "this" refers to the object being constructed, rather than a separate function scope.
Of course all the "recalc_property" methods still need to be written, and will use the corresponding "set_property" method to do the update, e.g.

  recalc_bendforce : function () {
    this.set_bendforce(
      this.calcbendforce ( this.tension, this.bendstones ));
  },
Since they might be needed by the graphing functionality each "calcProperty" method needs all the input parameters to be passed and doesn't attempt to use them from the class. For completeness, here's calcbendforce:

calcbendforce : function ( tension, semitones ) {
  var bendforce = tension * (Math.pow(2, semitones/6) - 1);
  return bendforce;
}
So either an update to the string tension or semitones (number of semitones to bend) needs to update the bendforce. In this particular model there's a hierarchy of variables that control others, so it's possible to have each parameter only need to update the next parameter in the chain.

Stringing it up

The interface is a fairly straightforward HTML page with Dojo input and display boxes attached to elements. To get it to talk to the physics model (which is defined as a Dojo class) each input object can be given a callback to run when the value is updated in the interface and can set a callback on the physics object to allow it to update the interface. Some elements don't depend on anything (like scale), so they don't set the callback in the physics object. Others can't be set and can only be updated as the result of other changes (like bend distance), so they don't use the set method.
String tension can both be set directly (bypassing the tension calculations) and updated by other changes:
tension_box is a "dijit/form/NumberSpinner" assigned to the element name "tension":
tension_box = new Spinner({
  onChange: tension_updated
  // other parameters
}, "tension");
tension_updated is a function which will update the wire tension when the value in the input box is being changed:
tension_updated = function () {
 if (tension_box.focused) {wire.set_tension(tension_box.get("value"));}
};
The "wire" callback is set so it can update the tension (for elements that are not input boxes the function can instead set the element value directly) when it changes in the model. Checking whether the box is focused prevents the callback trying to update the box while it's being edited.
wire.cb_updated_tension = function (){
  if (!tension_box.focused) {tension_box.set("value",wire.tension);}
};
And finally this has to appear somewhere, I've collected them in a startup function:
tension_box.startup(); 
Odd things happen if you forget to startup a widget, it may initialise controls, but refuse to honour settings like size.

Progression

In part 1 I mentioned that things were done in terms of tension. You can eliminate calculating the tension if you just want the bend distance, but since it's something that might be interesting (for string sets, or knowing how much harder a certain setup is to bend) instead a lot of the model works by finding the tension and using it to find the other numbers.
  1. Scale, string gauge, density and note are all used to calculate tension.
  2. The bend tonal distance is used to find how much extra tension is needed.
  3. Total string length (including past headstock), stiffness, gauge and bend tension are used to find the length stretch.
  4. Length stretch is used to find the sideways stretch.
  5. The sideways bend force needed is found using the total tension, sideways stretch and scale length.
Because of this it's also possible to update the tension independently to see what happens to the later numbers. For example maybe you're using the manufacturer's force numbers for a particular string set.

Graphs and hair loss

The graphs are done with Dojo's "Chart" widget, using the Lines type ("dojox/charting/plot2d/Lines"). One thing school science lessons should have drummed into you is that you need to label the axes on graphs. And this one isn't very useful:
What is the bend force for 25"? It's probably more than 24.5", but we can't even say that for sure, I might have gone crazy (or made a strange mistake) and put the scale upside down. I did ask it for labels (the "title" parameter). The problem turns out to be automatic ranges, if you don't give a "min" and "max" for the axes range, Dojo tries to calculate it from the data series you've given it.
I was starting the charts with an empty series to be filled in on the first update, the "scale length" x axis is okay, we can give it min and max since it doesn't change, but y needs to be automatic. Since Dojo can't guess and hasn't been given a range to start with it doesn't leave enough space for it once it gets the data. The solution is to generate the data when the chart is first created. Final result:

Hosting

Finally a quick note on hosting. At the time of writing it's 2013 and like many people I'm writing on a (free) web service. You can do all kinds of things with plugins, but just try deploying your own Javscript, or (actually worse) your own web page. Getting Javascript hosted by blogger or google sites is possible if you run through hoops, but if you need to go down this road and are happy to put your code under an open source license then Google Code is probably the simplest solution.
To use Dojo to do the page layout you need your own .html file, and that rules out most free websites and blogging. I've ended up using paid-for-hosting that I already had, but one interesting alternative is Amazon Web Services S3 which can host static content and provides 5GB free for the first year (though you need a credit card), even after a year hosting a few MB of JS (or even a few hundred) should cost less than a pound per year.
The finished product can be found at http://www.imalone.co.uk/stringbendcalc.html.

Comments

Popular posts from this blog

Peaceful tuning, Pacifica 112

Breaking in the Fender Mustang

Pure drumming