Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: .scl / .tun Retuning #828

Closed
bit-101010 opened this issue Apr 10, 2019 · 32 comments
Closed

Feature Request: .scl / .tun Retuning #828

bit-101010 opened this issue Apr 10, 2019 · 32 comments
Labels
Code Refactoring General code refactoring and cleanup issues like names, unused variables, warnings, fixme DSP Issues and feature requests related to sound generation in the synth Feature Request New feature request
Milestone

Comments

@bit-101010
Copy link

I would like to see alternate tuning options for oscillators, and perhaps everytihng that does key-tracking.

The most common standards for retuning are the AnaMark (.tun) and Scala (.scl / .kbm) formats. loading these should change the frequencies of each MIDI note.

There are some open tools for doing this already:
There is some C++ source code provided by Mark Henning (creator of the AnaMark standard) for using both formats of tuning files, but it handles .scl files incorrectly.
ZynAddSubFx handles .scl files as expected, but doesn't load TUN files. Microtonal.h/.cpp handles this function.

In addition (this is a stretch goal, and maybe beyond the scope of this initial request) being able to load and blend between multiple scales would open up a world of harmonic possibilities.

@baconpaul
Copy link
Collaborator

Oh that’s a really cool idea. The key to frequency code is fairly well isolated so it wouldn’t even be that tough

Question : surge modulates pitch by semitones like with an lfo or whatever. Wonder if other synths bind their modulation to the scale or use base note in scale space and frequency modulation in even temper space.

Obviously base note in scale space and frequency modulation in even temper space is way way easier!

@bit-101010
Copy link
Author

The synths I've worked with keep modulations by semitones.
Even pitch bend stays in semitones for some synths. It really comes down to implementation, which varies greatly for this stuff.
The main exception I've seen to the pitch bend staying in semitones is AnaMark, which gives you the option of using the pitch bend wheel to morph between two scales.

@baconpaul
Copy link
Collaborator

One other question: do you think we should have tuning per osc, per scene, or per patch? I can make all the arguments but interested how other synths do this.

@bit-101010
Copy link
Author

I'd go by scene or patch; loading somethings for each oscillator seems tedious.

@baconpaul
Copy link
Collaborator

So OK here's some notes on this issue

  1. Surge uses an internal representation of key most of the way down as basically "midi note + bend". So if you play a middle c with the pitch bend wheel half way up you get a pitch of "60.5" (60 being the midi for middle c; .5 being the pitch bend). MPE works the same just with different bend granularity

  2. The note is converted to frequency in the SurgeStorage function note_to_pitcn and note_to_pitch_inv which do linear interpolation of a static table which goes from integer notes to pitch in range -256 to +256. (note to self: Linear interpolating the inverse is not the same as the inverse of linear interpolation...)

  3. That function reads the table "table_pitch" which is a float alignas(16)[512] but is also a DLL global. And that's a bummer because it means the easy way to do tuning (read an .scl file; assume a .kbm which maps note 0 to middle 6 and rotate the frequencies through the scale into the table) will reset it for all surge instances in your process space. Since it is a DLL global.

So to do this the work is basically

  1. Make table_pitch and table_pitch_inv members of surge storage rather than globals; change all the callers of note_to_pitch and so on to have a storage reference they pass. (Or similarly squirrel it somewhere else that has the right alignment)

  2. Read the .scl file and slap it onto the table for that synth

  3. If the scl file has been loaded store it in the patch; if it has been read from the patch blat it onto the table. If there's no tuning, revert to standard tuning (that is even 100 cent tuning in scl speak)

  4. Add a UI gesture to load a tuning for the current patch

The tricky part is the moving of the table. Sort of a moderately nasty code refactor. But I kinda want to do this since I would learn a lot... so let me keep pondering a bit when and how to do that refactor.

@bit-101010
Copy link
Author

Makes sense, best of luck with the refactor.
On the other hand, having a way to retune all instances at once could be nifty...

@baconpaul
Copy link
Collaborator

Thanks!

And yea it would! But doing it accidentally because memory is shared is not the way to do it :)

@baconpaul baconpaul added the Feature Request New feature request label Apr 11, 2019
@baconpaul baconpaul added this to the 1.6.n milestone Apr 11, 2019
@baconpaul baconpaul added DSP Issues and feature requests related to sound generation in the synth Code Refactoring General code refactoring and cleanup issues like names, unused variables, warnings, fixme labels Apr 11, 2019
@baconpaul
Copy link
Collaborator

Huh that refactor wasn’t as bad as I thought. Just managed to make it work on a branch.

I’m interested in understanding this so may add this to my list for Sooner rather than later

Thanks for raising it

@bit-101010
Copy link
Author

By "this" do you mean the refactor/that part of the synth's inner workings? or the possibilities of retuning?
Either way, thanks for humoring me and looking into this.

@baconpaul
Copy link
Collaborator

The thing that was easy was the refactor

The thing I am interested - and have been interested in for a while - is the inner working of alternate tuning systems

So your issue has the wonderful combination of being code which isn’t a huge pita while also letting me learn about one of the things I want to know more about

So basically: it sounds fun to add retuning to surge. And it also sounds tractable. So I will do it this spring. But probably not this afternoon :)

@baconpaul
Copy link
Collaborator

So @bit-101010 I could use a favor.

I worked on this tonight some with SCL files. The headless app can actually play one of the goldberg variations to a wav file no problem so I don't have to deal with UI and streaming patch stuff. And I got it working with alternate scales. (I only tried 12 note scales)

Anyway this .zip file contains a standard midi file, a .scl tuning file, and the wav file that surge renders if it plays that midi file back with that tuning attached in my current branch.

tuning.zip

Is it possible for you to take a synth that you know to be a good renderer of midi with scl files and render out an audio file of it to see if we are doing the same tunings? I'm not quite sure how to test this but have been doing things like "printing velocities at the retune moment" and have confirmed that an .scl file with uniform tuning (so 100.0 200.0 etc...) doesn't change pitches and so on.

Would very much appreciate anything you can do to confirm that other synths with this .scl file render properly though

The branch is baconpaul/tuning-828 if you are interested but like I said the code isn't in the UI to actually use it yet so you have to be able to build and play around with headless to try stuff. Not ready for general use.

@baconpaul
Copy link
Collaborator

Hmmm one other thing you made me think. One of my rack modules is a quantizer. Now I know how this works I can make a VCV Rack module that quantizes CV to a .scl file step set also.

@bit-101010
Copy link
Author

I've had some ideas about that myself, but I've had trouble getting my Rack plugins to build. There is a whole world of tuning stuff that would make VCV rack even more powerful in my opinion.
The main thing I've had in mind is something like your HarMoNee that can do just intonation intervals.
Sometimes I feel like I'm on a crusade for the subminor third.

@baconpaul
Copy link
Collaborator

Ha! Well I linked the BaconPlugs issue to this one. Happy to chat on that issue tracker about ideas.

It would be very very easy though to do something like my polygnome where you have a CV input and you output that CV plus A/B. So if you want like the dodacahedral scale I used above you could just dial in the 19/12, 1732/1874 and so on. Basically an in-rack .scl editor for N tones.

But lets not spam up surge issue tracker with ideas for rack. Like I said happy to beat around ideas for those plugins.

@baconpaul
Copy link
Collaborator

Just a note to self

the table_pitch is calibrated as follows

table_pitch[0] = 1
table_pitch[12] = 2
table_pitch[24] = 4
table_pitch[36] = 8

you get the idea. These are the distances in multiplier from 0 which is why my tuning worked.

But which is the 'center' note. Well in a couple of places it is assumed the frequency is 16.3519783 * the table pitch.

That means we are tuned such that 1 = C0.

So if we want to tune to a 440 A constant and work our scales from there we need to make sure that note_to_pitch(57) = 26.9087 and then calibrate from there. Similarly if we want C3 constant then we need to make sure that well you get the idea.

This explains why my tuning works but my offsets are off. Not fixing it now just writing down my research for myself.

@baconpaul
Copy link
Collaborator

Your comments on slack were super handy; I've now fixed up my tuning and have it working.

My current choice is to keep C3 = 261.626hz a constant and use that as note 0. How do other synths do it in absence of a KBM file? Is it an option (so "hold A3=440; hold C4; hold C3")?

It gets weird when you chose a non-12-note scale too. Fun stuff.

@bit-101010
Copy link
Author

Probably not the best option given that multiple places assume a 16.3519783hz reference, but if you could make that value a variable, then you could just set it equal to the reference specified by the .kbm file. Then the fractions in a .scl file would translate directly to the values in the table. Start from the reference note and multiply up / divide down by the scale's formal octave (last degree in the scale) to fill it out. The cents-to-decimal calculations wouldn't be too bad to implement either.
Unfortunately, that means touching other parts of the code that could be avoided.

Without a .kbm file, you could just assume a reference note and pitch, but the implementations that I like let the user define those. For example, Zyn has parts in the GUI where you can set a reference even if you don't load a .kbm file.

@baconpaul
Copy link
Collaborator

Great
Surge assumes that pitch value 1 has frequency 16.3 and that note 0 has pitch value 1. Since we have a table we can break the later. So what I’m doing is keeping note 48 constant at 261.5 etc (or really note 48 constant at pitch 16). Then if I use a 6 note scale or something then note 0 ends up with a way lower pitch value.

Seems that 48 constant selection is what I need to let people pick absent a Kbm file

But what I really need to do is make it so you can try this and let me know if my implementation is ok! Maybe next week!

@bit-101010
Copy link
Author

Groovy!
If there's a way to load .kbm, that's all you need in terms of letting the user select a reference pitch. GUI for that is just an extra bonus.

@baconpaul
Copy link
Collaborator

Yeah I will look at kbm files next now I have scl working. Fun!

@SeanArchibald
Copy link

I'm now subscribed to this thread and happy to help test the .scl/.kbm implementation when it's available. @baconpaul do you still need someone to test that tuning.zip above?

@bit-101010
Copy link
Author

I think he has Tuning.zip testing under control (that conversation happened on the Surge Slack Channel).
The BaconTuning branch has retuning somewhat implemented, but it might be a bit before that gets finished and merged in.
Right now there's a lot of other v1.6 business, and recently these folks have started porting Surge to VCV Rack modules.

Whenever it does happen, it would be nice to have a tester with a good deal of retuning experience. Nice to have you on board, @SeanArchibald !

@baconpaul
Copy link
Collaborator

Yeah I ran out of time last week for other reasons the. Spent the weekend screwing around with vcv - this branch is mostly done. I’ll bring it back up the stack and try and get a beta into the nightlies

@baconpaul
Copy link
Collaborator

Just a reminder to myself from slack: http://sevish.com/scaleworkshop/

@baconpaul
Copy link
Collaborator

This was super useful. I was just incorrectly transferring ratios to cents (duh), and also assuming a 2.0. So with my push if you use the sin oscillator I think it is tuned correctly. And if you choose a non-sci file you don't get a core.

About 45 minutes until there's a new nightly. If you want to give a whirl next week that would be great. When I'm back I will look at the other oscillators and persisting the scale in the patch.

baconpaul added a commit that referenced this issue Jun 17, 2019
1: The tuning file was mis-interpreting fractions to cents convrersion
2: The tuning file wrapping assumed 2/1 as a last note in one place
3: If you choose a non-scl file you pine for the fjords

This addresses those, and moves us further along #828
@baconpaul
Copy link
Collaborator

Oh another feature we will need, when we save .scl in patches, is a menu item to "show current tuning file"

@baconpaul
Copy link
Collaborator

Alright I don't have a fix but I have a diagnosis on the mistuned classic oscillator

In the ::convolute method there's this code

   const float s = 0.99952f;
   float sync = min((float)l_sync.v, (12 + 72 + 72) - pitch);
   float t;
   if (oscdata->p[5].absolute)
      t = storage->note_to_pitch_inv(detune * pitchmult_inv * (1.f / 440.f) + sync);
   else
      t = storage->note_to_pitch_inv(detune + sync);

where t is used to set the rate of the voice. In normal operation when this is called, detune and sync are 0; so this is reading the "note 0" which is the "pitch 1" point in standard tuning. Basically this allows the entire tuning to shift by a bit and tune across the keyboard.

But this works remarkably poorly when you have a tiny scale like Q4.scl; at that point the "0" point (since the "48" point is held firm at 16) - then you read the super duper low frequency of the 0 point and apply that to your phase.

I can fix this by assuming the '0' point is 48 and the 'pitch' value there is 16 as follows

   float sync = min((float)l_sync.v, (12 + 72 + 72) - pitch);
   float t;
   if (oscdata->p[5].absolute)
      t = storage->note_to_pitch_inv(detune * pitchmult_inv * (1.f / 440.f) + sync);
   else
      t = storage->note_to_pitch_inv(detune + sync + 48) * 16;

You can see that re-centers the time to 48 as 16 rather than 0 as 1. And then the square wave oscillator on Q4 works just like the sin.

but I am totally not ready to commit that right now. There's so much testing I would have to do that it's not prudent this morning. So I'll leave this comment here so I don't forget.

baconpaul added a commit to baconpaul/surge that referenced this issue Jun 17, 2019
1. The concept of "48" being the center and "16" being its pitch
   were hardcoded. Make those inline functions in storage (which
   right now just return 48 and 16) and use those in the retuneToScale

2. The Classic oscillator convoultion for Square and Triangle
   assumed that note 0 was pitch 1; whereas we know note 48 is pitch
   16 and in regular tuning that is 2^(-48/12) lower. So adjust to
   use the center note as the convlution period time.

All of this is a no-op if you are in standard tuning.

Addresses surge-synthesizer#828
baconpaul added a commit that referenced this issue Jun 17, 2019
1. The concept of "48" being the center and "16" being its pitch
   were hardcoded. Make those inline functions in storage (which
   right now just return 48 and 16) and use those in the retuneToScale

2. The Classic oscillator convoultion for Square and Triangle
   assumed that note 0 was pitch 1; whereas we know note 48 is pitch
   16 and in regular tuning that is 2^(-48/12) lower. So adjust to
   use the center note as the convlution period time.

All of this is a no-op if you are in standard tuning.

Addresses #828
baconpaul added a commit to baconpaul/surge that referenced this issue Jul 10, 2019
As part of the feature request in surge-synthesizer#828, this begins to add
alternate non-uniform tunings to surge. This commit is a good
step along the way to the final feature set but is incomplete.

With this commit, you can return a surge to an .scl file of your
chosing, but you cannot load a kbm, cannot choose a center note
other than C4/261hz, and the scl file you choose does not save
into either the patch or the DAW and is transient.

I am adding this commit, though, so that (1) users more experienced
with tuning can test this in the nightlies, (2) I don't have to
keep rebasing it forward and (3) I am guaranteed to get this feature
properly into 1.6.2

Former-commit-id: 179cdb6686a9c9c123e3cc7cdf7fd642b496c647 [formerly ba855b9]
Former-commit-id: 970e82d8eb4e17990bbd66a3d396829aa269e870
Former-commit-id: a895b7d0e107a76a6c7035071cd1768a5df1859c
baconpaul added a commit to baconpaul/surge that referenced this issue Jul 10, 2019
1: The tuning file was mis-interpreting fractions to cents convrersion
2: The tuning file wrapping assumed 2/1 as a last note in one place
3: If you choose a non-scl file you pine for the fjords

This addresses those, and moves us further along surge-synthesizer#828
Former-commit-id: da475014cda92b2b27bc919aeb56bdfb8529bc32 [formerly e8f992b]
Former-commit-id: 10485a067909e3ba49e17e89cccb985acf1cdd31
Former-commit-id: a87946dde013c3c23a5fcb15b8f2086569db376c
baconpaul added a commit to baconpaul/surge that referenced this issue Jul 10, 2019
1. The concept of "48" being the center and "16" being its pitch
   were hardcoded. Make those inline functions in storage (which
   right now just return 48 and 16) and use those in the retuneToScale

2. The Classic oscillator convoultion for Square and Triangle
   assumed that note 0 was pitch 1; whereas we know note 48 is pitch
   16 and in regular tuning that is 2^(-48/12) lower. So adjust to
   use the center note as the convlution period time.

All of this is a no-op if you are in standard tuning.

Addresses surge-synthesizer#828

Former-commit-id: eb68e33df158bae9df35cbff3f8dc69e8c406748 [formerly 4ed615c]
Former-commit-id: 021a547900483225cdb6a37e67e0756fa3560112
Former-commit-id: 582f2f327546cfa1cc1c7345d6f16e37b109baa9
@baconpaul
Copy link
Collaborator

baconpaul commented Jul 14, 2019

Another “note to self” style comment as I get my part time summer music hacking organized.

OK I think the actual tuning in the nightly is correct so here’s a note to myself on what needs to happen to have this be finished

  1. Add an internal state variable on whether you are retuned with a .scl file (which init_tables sets to false and retuneWithScale sets to true) and an accessor on storage.
  2. Retain the raw text of the .scl file on the storage object
  3. Add a menu item to “show current tuning” which at least works on mac and windows to show the .scl file (and on linux dumps it to /tmp and pops up a message saying where to find it if url open to a file:// doesn’t work). Commit at the point that 1-3 are done and build a nightly.
  4. Stream the .scl file into and out of the patch (in association with the rest of A Host of things are not stored in patch master issue for 1.6.2 #915 things)
  5. Make sure that loading an old patch resets the tuning to standard (that is, if there is no tuning section, call init_tables)
  6. Make sure that loading a new patch without tuning does the same
  7. Make sure that loading a new patch with tuning applies it properly

If that all works then I will call “.scl file tuning done”. At that point I would open two more issues

  1. Support for .tun files (https://www.mark-henning.de/files/am/Tuning_File_V2_Doc.pdf)
  2. Support for .kbm files (http://www.huygens-fokker.org/scala/help.htm#mappings)
  3. An issue for the fascinating suggestion from @bit-101010 at the start of this thread to implement scale blending. That may be out of scope for surge ... but worth thinking about

Those 3 may get tagged 1.6.n not 1.6.2. I’ll see!

baconpaul added a commit to baconpaul/surge that referenced this issue Aug 13, 2019
1. The system knows if it is in tuning mode
2. You can see the tuning details (opens in a browser)
3. Menus work based on tuning state
4. Remove the experimental tag

Addresses surge-synthesizer#828
baconpaul added a commit that referenced this issue Aug 13, 2019
1. The system knows if it is in tuning mode
2. You can see the tuning details (opens in a browser)
3. Menus work based on tuning state
4. Remove the experimental tag

Addresses #828
baconpaul added a commit to baconpaul/surge that referenced this issue Aug 16, 2019
Some features of the synth - notably, zoom, MPE Enablement, and
Tuning - are features of the DAW environment you are working in and
were not persisted. This fixes that by adding a dawExtraState section
to the streaming protocol which is only populated and read at DAW time
not at general patch time.

Closes surge-synthesizer#890
Closes surge-synthesizer#914
CLoses surge-synthesizer#915
Mostly wraps up surge-synthesizer#828
baconpaul added a commit that referenced this issue Aug 16, 2019
Some features of the synth - notably, zoom, MPE Enablement, and
Tuning - are features of the DAW environment you are working in and
were not persisted. This fixes that by adding a dawExtraState section
to the streaming protocol which is only populated and read at DAW time
not at general patch time.

Closes #890
Closes #914
CLoses #915
Mostly wraps up #828
baconpaul added a commit to baconpaul/surge that referenced this issue Aug 17, 2019
This commit sets up a display of statuses in the synth so we
can bring to the front thigns like MPE and Tuning and bind the
menus more comprehensively. It also brings to bear the ability
(optionally) to store a tuning in a patch and the correct state
machine for what to do when loading a patch with tuning with
tuning active.

It almost completely addresses the remainder of surge-synthesizer#828
Addresses the streaming incompatability in surge-synthesizer#1035

Still outstanding is optionally a "lock" and to apply
the streaming version option to FX (which is surge-synthesizer#1037)

Handle streaming revision parameter set changes in OSCes. Deals with
baconpaul added a commit that referenced this issue Aug 17, 2019
This commit sets up a display of statuses in the synth so we
can bring to the front thigns like MPE and Tuning and bind the
menus more comprehensively. It also brings to bear the ability
(optionally) to store a tuning in a patch and the correct state
machine for what to do when loading a patch with tuning with
tuning active.

It almost completely addresses the remainder of #828
Addresses the streaming incompatability in #1035

Still outstanding is optionally a "lock" and to apply
the streaming version option to FX (which is #1037)

Handle streaming revision parameter set changes in OSCes. Deals with
@baconpaul
Copy link
Collaborator

OK with the push I did tonight .scl support works, is stored in the DAW, is optionally stored in a patch, has reasonable override semantics, shows its status in the UI, and tunes the keyboard properly, which was sort of the minimal viable tuning implementation. So to keep the 1.6.2 milestone chugging along, I added 4 issues with a “tuning” tag which are the work I would add in 1.6.3 probably, and am closing this one, since I think it’s now done!

What a cool thing to add to surge. Thanks for suggesting it and thanks for all the help testing and designing the feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Code Refactoring General code refactoring and cleanup issues like names, unused variables, warnings, fixme DSP Issues and feature requests related to sound generation in the synth Feature Request New feature request
Projects
None yet
Development

No branches or pull requests

3 participants