z_tuning (tuning mod for norns)

z_tuning

tuning mod for norns

current version: v0.1.0 (alpha 2)

from the README:


z_tuning

tuning mod for monome norns

  • use the normal install process for norns scripts: ;install https://github.com/catfact/z_tuning

  • with the mod enabled, tuning is applied globally, to all scripts which use the musicutil.note_num_to_freq library function and its variants.

    • caveat: unfortunately, not all scripts use this library; many hardcode their own MIDI->hz conversions and are not open to tuning adjustments.
    • caveat: ratio_to_note_num remains unchanged because it is mathematically unfeasible in the general case.
  • new tunings can be specified either:

    • using the Scala format (extension .scl),
    • or as Lua files (.lua).
  • .lua files defining scales can contain any lua code, but should return a table containing a field called ratios or a field called cents, either of which should be another table.

  • tuning data files should be placed in ~/dust/data/z_tuning/tunings/. this location will be populated with some “factory” files when the mod is first run.

  • tuning state (selection, root note, and base frequency) is saved on a clean shutdown with SLEEP, and restored on boot. this state is global for all scripts (for now.)

parameters

as of v0.2.0, z_tuning will add a group of parameters to the params list, following the script parameters. (this is awkward, but necessary to avoid corrupting existing script PSETs.)

these parameters can be saved and loaded as part of a PSET, thus allowing different scripts to be assigned different tuning configurations in a persistent manner.

the parameters are:

  • tuning: select a tuning ID from the list created at startup

  • root note (transposing): set the root note, without updateing the root frequency; this effects a transposition unless root frequency is also adjusted to match.

  • root note (adjusting): set the root note, and adjust the root frequency such that the new frequency value is what it would have been with the old root frequency under 12TET. (this sounds complicated, but it is the most intuitive way to change temperament independently of tuning offset.)

  • root note (pivoting): set the root note, and set the root frequency such that the new root note has its frequency unchanged. this is an unusual feature: it provides an interesting way to perform JI transpositions, but it is not an reversible operation!

  • root frequency: set the root frequency directly. this effects a transposition unless root note is also adjusted to match.

(note that setting any three paraneters for updating root note all cause each other’s values to refresh, but only the acting parameter produces side effects.)

mod menu usage

  • the mod menu exposes:
    • tuning selection,
    • specification of root note as MIDI number,
    • independent specification of base frequency

by default, base freq is coupled to root note; that is: whenever the root note changes, update the base frequency such that the tuning would not change under 12tet. hold down K3 to decouple them.

API usage

return the current state of the mod, a table containing:

  • tuning selection (ID string)
  • root note number
  • base frequency
    api.get_tuning_state()

return the entire collection of tuning data
api.get_tuning_data()

save/recall the current tuning state
api.save_state()
api.recall_state()

set the root note, as the root note parameter above
api.set_root_note(note)

set the root note, as with the root note (adjusting) parameter above
api.set_root_note_adjusting(note)

set the root note, as with the root note (pivoting) parameter above
api.set_root_note_pivoting(note)

set the current tuning, given numerical index
api.select_tuning_by_index(idx)

set the current tuning, given ID string
api.select_tuning_by_id(id)

add a new tuning table (e.g. constructed with Tuning.new)
(note that this will mess up existing tuning selection parameter values! this feature may be reconsidered.)
api.add_tuning(id, t)

get the deviation in semitones from 12EDO/A440, for a given note.
(could be useful for implementing tuning by MIDI pitch bend.)
api.get_bend_semitones(num)


  • see CHANGELIST.md for version information

  • see TODO.md for a list of known issues / development roadmap.

  • you may encounter bugs! please report them by visiting the github issue list. (feature requests are not needed.)


all original work is copyright ©ezra buchla and released into the public domain.

(note that this repo contains some .scl files downloaded from huygens-fokker.org. the licensing terms for this material are unclear to me, but it is freely available and builds on the work of others (e.g. composers), and i hope that sharing it here is in keeping with the spirit of the author’s intentions.)


58 Likes

thanks for pushing the extra efforts to release it properly.

i’ve been really enjoying it from the day i installed it.

5 Likes

i can’t believe this hasn’t gotten more responses. incredible idea. i can’t wait to try it out later today.

edit: nevermind! I didn’t know how to enable the mod. sorry, newbie here. figured it out.

edit2: tried it and I am totally thrilled. i love that i can change temperaments from the mod menu and hear the changes immediately. this makes me so happy i got a norns. thank you thank you thank you!

4 Likes

this went under my radar – glad it got bumped! this is absolutely wonderful and super useful! thanks zebra (and ngwese) : )

Thank you Devs. I can’t wait to dive in.
Similarly, I don’t know how this passed me by…

1 Like

Thank you again. This has really changed my approach to using Norns (I mostly was using it as a sequencer and not using the onboard synth stuff because of the lack of microtonal support). Thank you thank you thank you!

1 Like

This is so rad!!! Making this a mod was a super rad solution.

This is such a great mod-- I have a suggestion/idea: would it be possible to have the MIDI that Norns outputs follow the internal tuning? For example, I have norns outputting its midi to an expert sleepers fh2; it’d be great to have it retune the MIDI too. Is this possible?

i’m very glad you’re finding it useful, thanks for the feedback.

that isn’t really how MIDI works, note data is integers and doesn’t intrinsically express frequency. (it’s also why this mod can’t work with norns/supercollider instruments that adhere closely to the baseline MIDI keyboard paradigm and only consume note data directly.)

but it is possible for norns to control the tuning of MIDI synths that support MIDI Tuning Specification. basically you send a block of MIDI data when the temperament changes, that effectively retunes the instrument’s response to MIDI notes. unfortunately this part of the MIDI spec is not supported by a whole lot of hardware, fortunately for you the FH-2 does incluede MTS support as of firmware version 1.13.

i would take a PR to add this feature, but not very interested in it personally. (the basics are simple, but as always there are details to figure out. in this case i would be happy to illustrate the basics but i don’t have the setup or inclination to design and test for general use.)

(i should note for completness that one can attempt to use pitch-bend to implement microtuning, it can work for monosynths but really this is a method born of desperation and not a general solution.)

2 Likes

i have an half-baked fork of the mod which uses pitch bends.

(the code is clean, but there are issues w/ the bend amount calculations, i.e. bad maths).

fwiw, midihub does microtuning through pitch bend and it works fairly well. the adavantage is that it’s supported by the vast majority of older synths out there.

there’s of course the polyphony limitation, but a good chunk of sequencers on norns output mono sequences anyway.

EDIT: here is a link to my fork. it “works” but the bend values are off.
for those that may want to implement a MTS-based solution here is the relevant bit regarding overriding midi out behavior.

2 Likes

cool, well if it works well enough then happy to add it of course, and i’ll take a look at the maths

for MTS, i don’t think you actuall,y need to override anything; if i understand correctly it is just a chunk of data that is sent once when the tuning changes.

(but, idk, i’m having trouble logging in / signing up to retreive this document from midi.org)

i’d propose adding a toggle param to the mod that, when engaged, cuases all connect3ed devices to receive MTS whenever temperament / root changes.

anyways i have a couple more changes to push before i consider the basic functionality complete…


[ed] @eigen here’s the (untested) math i would use.

-- helper
function log2(x) 
    return math.log(x) / 0.69314718055995 -- math.log(2)
end

hz_12tet = root_freq * (2 ^ ((midi - root_note) / 12))

hz_tuned = ... -- computed from table

--- then there are two ways to do this:

--- shown: take the ratio between 12tet and tuned freqs, convert that to ST
ratio_tuned = hz_tuned / hz_12tet
st_tuned = log2(ratio_tuned) / 12

--- (not shown: convert the tuned freq to STs, take the difference)

return st_tuned / bend_range * 16384

that’s a lot of operations. fortunately there is a major optimization available: each degree ends up with a specific bend amount (deviation from 12tet) that does not change and is the same in every (pseudo-)octave. so the bend offsets can simply be calculated once when the tuning changes and kept in a table. (this could also be useful for visualizing tunings.)

2 Likes

thanks a bunch :grin:

so i’ve been trying this implem but get some fishy results (too low st delta values).

e.g. for ji_ptolemaic, at note #70:

hz_12tet=466.16376151809
hz_tuned=469.33333333333
ratio_tuned=1.0067992668605
st_tuned=0.000814672588179

(assuming bend_range = 4 semitones => 2 * 2 halves)
st_tuned / bend_range = st_tuned / 2 = 0.0004073362940895

which seem too low of a value.

assuming it’s normalized around -1/1, (0.0004073362940895 + 1) * 8192 would give close to no pitch bend at all.

i guess there is some missing normalization/denormalization or a typo in one up there.


also unrelated, lua now has math.log(x, 2) for log2. it seems to behave the same as the implem you posted.

yes sorry there is a typo, /12 instead of *12

so i actually began prepping a v0.1.0, and added a feature where changing tuning params now updates a memoized “bend table” in the tuning state. then an API is exposed called get_bend_semitones which gives the deviation of a given MIDI note from 12tet/A=440 under the current tuning.

so that gets you most of the way there towards the MIDI bend mapping. there are a couple details you might need/want to work around:

  • this bend amount includes displacement from tuning the root freq and root note separately. (so that can cause it to exceed 1st or any other arbitrary interval.)

  • when the tuning has >12 degrees per octave, the bend amount gets large pertty quick!

(the workaround in each case would just involve decomposing larger bend amounts into a transposition plus a bend.)


anyways, the bend table stuff is in a branch called v0.1.0-wip if you want to check it out. quick tests of the feature look OK. will push the version when i get a chance to add parameters and document.


ah, thanks. its a little OCD but i’d want that to use a constant for log(2) rather than performing 2x logs. (perhaps lua does so)

4 Likes

yes! it’s working now.

i have yet to try w/ the v0.1.0 branch.

i just had so much fun w/ just awake & an external synth + this mod.

for those that may want to try it, i’ve committed the fixes to my forked repo.


MTS

so do I. found this digest that even comes w/ a sc implem:


log2

it doesn’t use a constant on Linux.
there seems to be a special implem of log2 that may use one but it only activates for windows builds.

image

2 Likes

For comparison, in pitfalls, you can play a scale from a given EDO via grid or MIDI in.

The 12 keys in a octave on a keyboard are mapped to scale degrees in the scale based on a mapping file.

E.g. here’s 12 keys mapped to notes in a pentatonic scale’s 5 scale degrees:

  [5] = {
    [1] = 1, # white C key -> 1st scale degree
    [2] = 1, # black C# key -> 1st scale deg.
    [3] = 2,
    [4] = 2,
    [5] = 2,
    [6] = 3,
    [7] = 3,
    [8] = 4,
    [9] = 4,
   [10] = 5,
   [11] = 5,
   [12] = 5
  },

A MIDI in note is mapped to a scale degree and octave.

Then the pitch frequency is looked up based on that scale degree and octave.

function midi_in.pitch(note)
  return pitches:octdegfreq(midi_in.oct(note), midi_in.deg(note))
end

function midi_in.oct(note)
  return note // 12 - 1
end

function midi_in.deg(note)
  return keyboard_mappings[scale.length][note % 12 + 1]
end

For playing a given pitch via MIDI out, pitch frequency is converted to a fractional MIDI note.

local base = 440/32
function pf.midi_to_hz(note)
  return base * (2 ^ ((note - 9) / 12))
end

local denom = math.log(2)
function pf.hz_to_midi(freq)
  return 12 * (math.log(freq / 440.0) / denom) + 69
end

Then the fractional MIDI note is converted to a pitch bend value

function midi_out.pitch_bend_value(midi, pitchbend_semitones)
  local semitone_delta = midi - math.floor(midi)
  if semitone_delta == 0 then
    return 0
  else
    -- MIDI pitch bend is +8192/-8191.
    return (8192 * semitone_delta) / pitchbend_semitones
  end
end

Then note on and pitchbend can be sent to the MIDI out device.

midi_out.note_on_pitch_bend(freq)
    local note_num = midi_out.hz_to_midi(freq)
    pitchbend_semitones = params:get("pitfalls_pitchbend_semitones")
    local bend = midi_out.pitch_bend_value(note_num, pitchbend_semitones)
    midi_out_device:note_on(math.floor(note_num), 95, midi_out_channel)
    -- print(bend)
    midi_out_device:pitchbend(math.floor(bend), midi_out_channel)

    table.insert(active_notes, math.floor(note_num))

end

I hope that was of interest. :notes:

4 Likes

@delineator this is really interesting. i don’t know why/how i missed pitfalls.

both z_tuning and pitfalls seem to play on the same principles but nonetheless complementary (scala parsing & global effect VS manually altering a tuning w/ a pretty display of info).
it could be nice to have z_tuning not take effect when it detects that current script is pitfalls.

also, my theory around all this is very lacking so excuse the stupid question: how do the named scales relate to the tunings?
are they synonymous w/ a “tuning” (i.e. fixed Hz values according to root note) or is tuning a decorrelated param (Hz values would vary depending on the active one)?

The named scales are parsed from Scala’s list of musical modes. They are stored and displayed in Ls notation in pitfalls.

In pitfalls, pitch tuning is derived from a base frequency e.g. 440hz, a fixed starting note e.g. A, and the size of the EDO (equal divisions of the octave) of the given Ls notation scale.

This tuning approach is my interpretation inspired by the work of Erv Wilson, Stephen Weigel, and others. Mea culpa if I’ve misinterpreted their work.

The Xen wiki calls the Lsnotation sequence a Word - an abstract scale template with some specified pattern of step sizes.

For example, the western Major scale is:

LLsLLLs - with L=2, s=1

That gives us 12 EDO (=2+2+1+2+2+2+1).

A 6 note scale I like called Gorgo-6 can be represented as:

LLsLLL - with L=3, s=1

That’s 19 EDO (=3+3+1+3+3+3+1).

With pitfalls you can play the notes from the scale via grid or MIDI keyboard. The scale notes are a subset of all the notes in a given EDO tuning.

2 Likes

Really interesting and useful mod! Thank you @zebra

This doesn’t seem to work with sines, though sines does use the MusicUtil.note_num_to_freq() method to set frequency in sc. maiden reports z_tuning: init menu? but nothing happens.

Any ideas?

perhaps a problem with the the (de)tune function?

function tune(synth_num, value)
	--calculate new tuned value from cents value + midi note
	--https://music.stackexchange.com/questions/17566/how-to-calculate-the-difference-in-cents-between-a-note-and-an-arbitrary-frequen
	local detuned_freq = (math.pow(10, value/3986))*MusicUtil.note_num_to_freq(notes[synth_num])
	--round to 2 decimal points
	detuned_freq = math.floor((detuned_freq) * 10 / 10)
	set_freq(synth_num, detuned_freq)
	edit = synth_num
	screen_dirty = true
end

it’s working fine for me.

one thing that may be surprising is that changing the tuning doesn’t immediately change the sound of the oscillators. it requires you to “goose” the tuning amount for each oscillator, because that is when the script will actually calculate frequency values

1 Like

That is good to know as I always restarted the script to make it work. I thought it was to be expected in my notoriously pragmatic mind. :slightly_smiling_face: