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

Fix automated base notes and their automations (#6548) #6725

Merged

Conversation

michaelgregorius
Copy link
Contributor

Fix all base notes that are used in automations and their corresponding automation values. Base notes that are automated are stored as elements in the save file whereas non-automated base notes are stored as attributes. So far the method upgrade_extendedNoteRange only upgraded the non-automated base notes that are stored in attributes. This commit implements the upgrades of the automated ones which are stored in elements.

The fix works as follows:

  • Collect all base note elements.
  • Store their ids in a set so that we can later identify automations that reference them.
  • Collect all automation pattern and check if they reference a base note.
  • Adjust the values and out values of all automations that reference base notes.

Note: for many older files the out values will be introduced by the upgrade method upgrade_automationNodes and do not appear in the files themselves!

Fix all base notes that are used in automations and their corresponding
automation values. Base notes that are automated are stored as elements
in the save file whereas non-automated base notes are stored as
attributes. So far the method `upgrade_extendedNoteRange` only upgraded
the non-automated base notes that are stored in attributes. This commit
fixes the automated ones which are stored in elements.

The fix works as follows:
* Collect all base note elements.
* Store their ids in a set so that we can later identify automations
  that reference them.
* Collect all automation pattern and check if they reference a base
  note.
* Adjust the values and out values of all automations that reference
  base notes.

Note: for many older files the out values will be introduced by the
upgrade `method upgrade_automationNodes` and do not appear in the files
themselves!
Remove debug code from DataFile::upgrade which was accidentally
committed.
@Veratil
Copy link
Contributor

Veratil commented Jun 5, 2023

Will the upgrade function still be run if someone already had their project updated? I'm pretty sure our upgrade functions don't run on projects twice?

@Spekular
Copy link
Member

Spekular commented Jun 5, 2023

If it's in the same function the upgrade won't re-run. Breaking it out into a separate function would make it run, at the cost of DataFile.cpp being permanently uglier.

However, I'm not sure how likely it is that someone would upgrade to a 1.3 alpha/nightly, discover that instruments are playing in the wrong octave, and save over the file without fixing it. If they go back to the 1.2 project file, the fixed upgrade routine will run whenever they next try switching to 1.3. If they move to 1.3 anyway and manually fix the octave, the upgrade routine would be redundant. So maybe it's fine if it doesn't re-run?

@Veratil
Copy link
Contributor

Veratil commented Jun 5, 2023

If it's in the same function the upgrade won't re-run. Breaking it out into a separate function would make it run, at the cost of DataFile.cpp being permanently uglier.

I don't think the files' beauty matters when we're trying to make sure things are fixed. 😉

However, I'm not sure how likely it is that someone would upgrade to a 1.3 alpha/nightly, discover that instruments are playing in the wrong octave, and save over the file without fixing it.

I agree, but there are a number of people who have been working with 1.3 downloads. I wouldn't be surprised if there is more than one person that had this happen to their project.

If they go back to the 1.2 project file, the fixed upgrade routine will run whenever they next try switching to 1.3. If they move to 1.3 anyway and manually fix the octave, the upgrade routine would be redundant. So maybe it's fine if it doesn't re-run?

That's what I'm trying to decide, do we want to just say "if you already opened past this commit then it's up to you to fix" or "we want to maintain proper upgrade routines no matter what".

@he29-net
Copy link
Contributor

he29-net commented Jun 5, 2023

I think the code may be missing one important step: checking whether the automation track drives a base note of an instrument affected by the original non-standard MIDI range (#1857), or some other instrument.

If the automated base note belongs to an affected instrument (see auto affected = [](const QDomElement& instrument) in the beginning of the update routine), it should not be touched. It already sounded an octave lower in <= 1.2.2, so it must stay the same in 1.3.x. Automation of all other instruments, on the other hand, should be updated to counteract the change in base note number.

And it gets even more unpleasant: if an automation track drives multiple base notes, it may be necessary to create two versions: one corrected, and one unchanged. The affected instruments should stay connected to the original, and the others should be re-connected to the new, corrected version.

There may also be some extreme edge cases, like if someone uses one automation track to drive several unrelated targets, like a base note and volume, or perhaps more realistically, base note and Master pitch or something. Hopefully, if changes are only ever made to a new copy of the automation track, I suppose it should leave all such "unreasonable scenarios" reasonably intact... Otherwise I don't even want to guess what to do with such cases, other than perhaps make sure the updated base note won't fall of range.

@Spekular
Copy link
Member

Spekular commented Jun 5, 2023

I don't think the files' beauty matters when we're trying to make sure things are fixed. 😉

I'm mostly thinking about the precedent of going out of our way to support backwards compatibility between unstable versions. Of course 1.3 should properly upgrade 1.2 files, but how reasonable is it really to expect 1.3-[long commit hash a] to upgrade a 1.3-[long commit hash b] file that was saved in a broken state?

That's what I'm trying to decide, do we want to just say "if you already opened past this commit then it's up to you to fix" or "we want to maintain proper upgrade routines no matter what".

I don't mean to compare "proper" vs "unproper"; what I wanted to point out was that any projects a user has already fixed after upgrading would be broken if the base note is shifted again by a new upgrade routine.

@Veratil
Copy link
Contributor

Veratil commented Jun 6, 2023

I'm mostly thinking about the precedent of going out of our way to support backwards compatibility between unstable versions.

I was under the impression that we were moving forward with the idea that master is a "stable" version? People could expect that master will be kept as stable as possible.

Of course 1.3 should properly upgrade 1.2 files, but how reasonable is it really to expect 1.3-[long commit hash a] to upgrade a 1.3-[long commit hash b] file that was saved in a broken state?

As reasonable as we want, honestly. There's old upgrade routines that are literally days between.

I don't mean to compare "proper" vs "unproper"; what I wanted to point out was that any projects a user has already fixed after upgrading would be broken if the base note is shifted again by a new upgrade routine.

This is a good point I didn't think of. So really either way we go we have the possibility of breaking something.

@michaelgregorius
Copy link
Contributor Author

Hi @Veratil, @Spekular, @he29-net,

thanks for checking the PR and giving input! So it seems that the situation can be summarized as follows.

New version or not

It seems we have two options on how the added code is run:

  1. Run it in an extra method and add a new version. The consequence is that manually fixed projects saved with a version that has "passed" DataFile::upgrade_extendedNoteRange will be "unfixed" by the code and messed up into the other direction, i.e. the manually corrected notes appear raised by an octave.
  2. Run the added code in the existing method and keep the latest version. The consequence is that files that have "passed" DataFile::upgrade_extendedNoteRange will still be affected by the bug reported in Audio file transposed an octave down between versions #6548, i.e. the automations that were not fixed will pull down the notes.

So it seems that we have to decide which is the lesser of the two evils. I think we should decide based on what the less probable scenario.

If we decided to go with a new version please advise on how to do so. Adding the new method at the end of DataFile::UPGRADE_METHODS should be straight forward. But how is the version number itself bumped during the upgrade?

Additional checks

The check for the affected instruments is missing but it should be possible to add it. I think in this case the code would look somewhat as follows:

  1. Search all instruments and only continue with a potential fix if they are not found by the lambda affected. If they are found stop here and continue with the other instruments.
  2. To potentially fix an instrument search for a sibling element of type "basenote". This assumes that each "instrumenttrack" can only have one "instrument" and one "basenote". If it could have several instruments of different types it would not be possible to fix the single "basenote" if the instruments differed in type and are part affected and part not.
  3. If the sibling element is found it is corrected, put into the set of automated base notes and then the automations are updated like already implemented.

@he29-net wrote:

if an automation track drives multiple base notes

Can an automation track drive more that one modulation target? If yes, how can I set this up in LMMS to create a test file?

Questions about the current upgrade method

@he29-net , why is only the first instrument taken into account? See line 1700 in current master:

QDomElement instrument = instruments.item(0).toElement();

Can each track only have one instrument beneath it? Or is this a potential bug?

In line 1703 a "basenote" attribute is added to the instrumenttrack without checking if the "basenote" is stored as an attribute (non-automated) or as an element (automated). For example the test file provided in #6548 has the following section:

<instrumenttrack usemasterpitch="1" vol="100" pitch="0" pan="0" pitchrange="1" fxch="0">
  <basenote id="29389" scale_type="linear" value="57"/>
  <instrument name="audiofileprocessor">
    <audiofileprocessor amp="100" sframe="0.244634" eframe="0.592715" src="/path/to/Snap.ogg" looped="0" lframe="0.257909" reversed="0" stutter="0" interp="1"/>
  </instrument>

Depending on what instrument.parentNode().toElement().attribute("basenote").toInt() + 12 does for a non-existing attribute "basenote" the example XML might have two basenotes after the fix: one attribute "basenote" and one element.

@he29-net
Copy link
Contributor

he29-net commented Jun 6, 2023

@michaelgregorius

Can an automation track drive more that one modulation target? If yes, how can I set this up in LMMS to create a test file?

That should be easy to set up; just hold Ctrl and drag multiple controls, one after another, to the same automation clip. Then if you right-click on the clip, there should be an "# Connections →" entry, which contains a list of all connected controls.

QDomElement instrument = instruments.item(0).toElement();
Can each track only have one instrument beneath it? Or is this a potential bug?

I am not 100 % sure, but I personally have no idea how I would put two instruments into one instrument track. I don't think it is possible in the GUI, so my guess is that if you try to create such an instrument track by editing the XML file, only the first instrument will play, or something breaks. Unless multiple instruments in one (non-BB / non-Pattern) track were supported in some ancient LMMS version I'm not aware of, and the core still happens to supports it...

Depending on what instrument.parentNode().toElement().attribute("basenote").toInt() + 12 does for a non-existing attribute "basenote" the example XML might have two basenotes after the fix: one attribute "basenote" and one element.

That's a good find; I had no idea that the basenote attribute could sometimes be missing and / or become an element instead, so I did not take that into account. I don't really know much about the internals of automation, so I'm not sure what would happen when two different basenote entries are present. Probably nothing good. ^^;

Given this oversight could lead to some potential weirdness, I would personally lean on the side of fixing and extending the current DataFile::upgrade_extendedNoteRange function, rather than creating a new upgrade routine just for the automation fix. But I have no insight to the number of users that may have already upgraded their projects from 1.2.2 to 1.3.x alpha, so I can't really help with deciding which approach would be less disruptive..

@Spekular
Copy link
Member

Spekular commented Jun 6, 2023

If we decided to go with a new version please advise on how to do so. Adding the new method at the end of DataFile::UPGRADE_METHODS should be straight forward. But how is the version number itself bumped during the upgrade?

This is handled automatically in DataFile::upgrade() with the help of the UPGRADE_METHODS vector as of #5660.

m_fileVersion = UPGRADE_METHODS.size();

@michaelgregorius
Copy link
Contributor Author

@he29-net, thanks to your help I was able to setup a more complex example file. With that file I now also came to the realization that a complete fix would indeed include a potential duplication of automation patterns into ones that contain unaffected elements and ones that contain affected ones.

I also found some other potential problems with that example file. Modulated elements, e.g. base notes, do not seem to be reset to their nominal values after playing. This means that if you modulate a base note with a linear automation, play the song and save the file then the base note is very likely saved with a float value. In my example file there's a "basenote" with a value of 57.200001. I think this could create problems when they are cast to int, corrected and then written to the attributes/elements.

I also wonder why BB tracks are ignored? In my example file I have a BB track that container with a TripleOscillator that has an automated base note.

And why are the base notes fixed unconditionally but the notes are fixed conditionally?

Another potential problem in the upgrade method is that there are nested for loops that all have i as their running variable.

I agree that it might be better to fix DataFile::upgrade_extendedNoteRange and to not introduce a new version. If people work with alpha versions and/or development builds they should expect that things might break and not save over their save files saved with a stable version.

@he29-net
Copy link
Contributor

he29-net commented Jun 7, 2023

And why are the base notes fixed unconditionally but the notes are fixed conditionally?

The notes are fixed so that the their labels in piano roll match the tones that will be played (assuming default 12 EDO tuning). Otherwise the update routine could have been be done by only changing base notes -- but the labels would be off.

Basically, the base notes are shifted one octave up, making everything sound one octave lower. Then, notes of affected instruments are kept without change in order to preserve the buggy <= 1.2.2 behavior (while reflecting the fact they sound an octave lower in the label, i.e., old LMMS note label 57 = A4 becomes standard MIDI 57 = A3), while all the other notes are shifted one octave higher to counteract the previous unconditional base note change.

As for the ignored BB tracks, I think it's because the "container track" (that is being ignored according to the comment) does not contain any instruments. It just contains other, "proper" tracks, that are processed by the subsequent code.

Move the fix for the automated base notes to the code that fixes the
non-automated base notes.

Improve the fix for automation tracks and patterns by potentially
splitting them into two tracks:
* The original track which is adjusted to only contain patterns with
targets that are not base notes.
* A cloned track that only contains patterns with base note targets.

This is done by iterating over all automation tracks and checking which
types of automations they contain:
* Base note automations
* Automations of other targets

The result for each automation track are then evaluated as follows.
* If an automation track does not contain any base note automations it
is kept as it is, i.e. nothing is done.
* If an automation track only contains patterns with base note
automations its patterns are corrected in place.
* If an automation track contains patterns with base note automations
and other targets then the track is duplicated so that we can split it
as described above. This split and correction is done on a per pattern
level. Patterns that would become empty are removed.

Cloned tracks will keep the same names and attributes as their original
tracks.

TODOs
------
* Base notes are read as integers, corrected by an integer value of 12
and then stored back as an integer. In some files base notes have float
values, i.e. if the file was stored after the base notes have been
automated linearly.
* B&B tracks are not corrected although they can contain instruments
with (automated) base notes!
* Nested for-loops with "i" as their running variables. (Fix in a
separate commit)
The GitHub builds seem to need the explicit include which for some
reason is not needed on my developer machine.
Fix the problem with the nested for loops that all had variables called
"i" by extracting a helper method with a corrected nesting. Due the
extraction we do not have to care anymore if the correction is running
beneath another for loop with potentially the same variable names.
Fix the base notes and automations of B+B tracks.

This fixes for example the problem that when a TripleOscillator had been
put into a B+B track, e.g. with version 1.2 that it sounded too high
when being loaded with a current master version. The cause seems to be
that the notes of the instrument pattern are corrected/transposed too
often (likely due to the collection of all tracks starting from the
song/document root).

The multiple correction of notes is fixed by traversing the song
structure in a more consise way. First all track containers of the song
are collected and for each of them the tracks are collected. These
tracks are then fixed one by one. B+B tracks are getting a special
handling while doing so. It is assumed that a B+B track cannot have
other B+B tracks inside and therefore all sub tracks of the B+B track
are searched for so that they can be fixed recursively.

Refactor some more functionality into static helper methods:
* Everything beneath a track is now fixed by the helper method
`fixTrack`.
* The fix of the base notes of the instruments themselves and the
extraction of the ids of automated base notes has been moved into
`fixInstrumentBaseNoteAndCollectIds`.
* The lambda `affected` has been converted into a static method because
it is must be accessible by one of the static helper methods.
* The code for fixing the automation tracks of a song has been moved
into the helper function `fixAutomationTracks`.
@michaelgregorius
Copy link
Contributor Author

@he29-net, @Veratil, @Spekular,

I have pushed some more commits which fix the B+B tracks and which also implement the splitting of mixed automation tracks.

If an automation tracks automates base notes and other targets it is now split into two tracks. The first one contains all the automation for the other targets and the second one the corrected automation for all base note targets.

If an automation does not contain any base note automations it is not touched. If it only contains base note automations it is fixed in place.

I have used the following file to test the fixes: MultiTargets-1.2.zip. If you play that file in the current master you will find that most tracks sound different than in LMMS 1.2.2. If you load it with this branch it should sound exactly the same. The sound/behavior of presets has not been changed between this branch and master.

Can you please test as well?

@michaelgregorius michaelgregorius added the needs code review A functional code review is currently required for this PR label Jun 11, 2023
@michaelgregorius michaelgregorius added this to the 1.3 milestone Jun 11, 2023
@michaelgregorius michaelgregorius linked an issue Jun 11, 2023 that may be closed by this pull request
Copy link
Contributor

@he29-net he29-net left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I finally finished going through the code, and apart from a few details (3x while(!object.isNull()) missing a space after while, and a typo on line 1826 "because they container a track container"), the only small issue I see are the names of helper functions. affected(), fixTrack(), fixAutomationPattern() etc. would be now visible to all upgrade routines added in the future, since they are not just a local lambda like affected() was before.

Maybe they could be prefixed by midiRange or something. Or maybe it does not matter, because DataFile.cpp is not that complex so it won't cause much trouble. So I'm not insisting on fixing that; just wanted to point it out in case it was missed.

As for the TODO regarding float values on non-automated basenotes; it seems that the base note is loaded into an integer automatable model, so any decimal part would be discarded after a load from XML anyway? I also don't see how could a float make its way into the XML in the first place, if automation isn't involved. So I would say the current code is OK.

Other than that, I like how it is all structured. The upgrade routine got pretty complex, but separating the tasks into multiple functions and commenting on what's being done and why makes the process easy to follow and understand. 👍 I tested it with the supplied file about a week ago and I do not recall any issues, so as far as I'm concerned this PR should be good to go.

Add a space after some while loops. Fix a typo in a comment.
Extract the code that upgrades the extended note range into its own
class. This hides the helper functions that are related to the upgrade
from the other upgrade methods in DataFile.cpp.

The header file is put into the src/core directory as it is not part of
the public interface and therefore should not be included in the
"global" include directory. The whole thing is just an implementation
detail of the upgrade in DataFile.cpp.
@michaelgregorius
Copy link
Contributor Author

Hi @he29-net,

thanks for the review.!

I have fixed the while statements and the typo with commit dac1f77.

Commit 5335f6d then moves the whole upgrade code for the extended note range into its own class to hide all the helper methods. I have added the header file to the src/core folder because it is not part of the public interface and thus should not go into the "global" include directory.

Just as I am typing this I see that the mingw builds are now failing. 😅

Add the comment "// namespace lmms" to the closing scope of the lmms
namespaces. The "scripted-checks" are quite strict. Perhaps they should
be renamed to "strict-checks". ;)

Include "cassert" in UpgradeExtendedNoteRange.cpp to hopefully fix the
mingw builds.
@michaelgregorius michaelgregorius merged commit 4ff9507 into LMMS:master Aug 16, 2023
@michaelgregorius michaelgregorius deleted the 6548-FixBaseNoteAutomations branch August 16, 2023 19:14
michaelgregorius added a commit to michaelgregorius/lmms that referenced this pull request Aug 27, 2023
Towards the end of the development for the fix of LMMS#6548 (via LMMS#6725) the upgrade code was refactored into its own class. While doing so it was forgotten to actually call the `upgrade` method on the `UpgradeExtendedNoteRange` instance. As a result almost all files should currently open in a wrong state with many instruments transposed. This commit fixes this.

Also explicitly check the assertion that BB tracks do not contain other BB tracks.
Copy link
Member

@messmerd messmerd Aug 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should have gone under include/ not src/core/.

EDIT: I just saw the comment made previously about this subject.. I guess I understand, but I still don't know how I feel about it. Maybe an include/private/ directory would be a good place for private headers such as this. I've seen other repos do it that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @messmerd,

yes, it seems like LMMS is currently missing a concept for these things.

If LMMS for example had a clean separation between the core and the GUI I would assume that only the core classes would be part of a public interface because they would be the most "interesting" and "open" ones to program again. Clients could for example use them to implement their own command line interfaces, reuse DSP algorithms, etc. I guess it would not be too helpful to have the interface of a very specialized GUI dialog in the include directory because what should clients really do with it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug needs code review A functional code review is currently required for this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Audio file transposed an octave down between versions
5 participants