Skip to content

Initial TablEdit importer #29242

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

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open

Initial TablEdit importer #29242

wants to merge 13 commits into from

Conversation

lvinken
Copy link
Contributor

@lvinken lvinken commented Aug 13, 2025

Resolves: #26144

Initial (and still incomplete) TablEdit File Format 3.00+ importer. Merged into a single commit, full history still available in my import_tef_baseline_master_20250812_ae66071 branch.

Will continue working on completing the implementation, but want to prevent a complete big-bang pull request. As is, the import is already usable.

  • I signed the CLA
  • The title of the PR describes the problem it addresses
  • Each commit's message describes its purpose and effects, and references the issue it resolves
  • If changes are extensive, there is a sequence of easily reviewable commits
  • The code in the PR follows the coding rules
  • There are no unnecessary changes
  • The code compiles and runs on my machine, preferably after each commit individually
  • I created a unit test or vtest to verify the changes I made (if applicable)

@lvinken
Copy link
Contributor Author

lvinken commented Aug 13, 2025

Will fix the codestyle issues (but not tonight :-) ).
Done: ran uncrustify 0.73.0 on all files in src/importexport/tabledit.

Copy link
Member

@cbjeukendrup cbjeukendrup left a comment

Choose a reason for hiding this comment

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

This looks great! A few minor comments.

LOGD("tbed %d wOldNum %d wFormat %d", tefHeader.tbed, tefHeader.wOldNum, tefHeader.wFormat);
if ((tefHeader.wFormat >> 8) < 10) {
return Err::FileBadFormat;
//return Err::FileTooOld; // TODO: message is too specific for MuseScore format
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps we should indeed somehow refactor the error handling so that engraving::Err no longer has to be used outside engraving (this also applies to other importers). But that's probably something for a next PR.

void TablEdit::createTitleFrame()
{
VBox* vbox = Factory::createTitleVBox(score->dummy()->system());
vbox->setTick(mu::engraving::Fraction(0, 1));
Copy link
Member

Choose a reason for hiding this comment

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

Perhaps you need to call vbox->manageExclusionFromParts(/*exclude*/ false);, like in MasterNotation::applyOptions for new scores.

else {
LOGD("measure %p", measure);
}
Segment* segment { measure->getSegment(mu::engraving::SegmentType::ChordRest, tick) };
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this be Segment::CHORD_REST_OR_TIME_TICK_TYPE, for the situation that the text doesn't coincide with a Chord? Or will it always do so?

instrString str { pitch };
stringData.stringList().push_back(str);
}
part->instrument()->setStringData(stringData);
Copy link
Member

Choose a reason for hiding this comment

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

For playback to work correctly, we'll also need to set an instrument ID. See perhaps searchTemplateForInstrNameList, but maybe there are better ways in this case.

void TablEdit::createNotesFrame()
{
if (!tefHeader.notes.empty()) {
VBox* vbox = Factory::createTitleVBox(score->dummy()->system());
Copy link
Member

Choose a reason for hiding this comment

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

Or would TBox perhaps be appropriate here?

Copy link
Member

Choose a reason for hiding this comment

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

GitHub displays a couple of compiler warnings in this file

@cbjeukendrup
Copy link
Member

Regarding timing/releases: given that the feature freeze phase for 4.6 has been entered last week, and that this import is still a work in progress, this will likely have to be a 4.7 feature. Would that be okay with you?

@lvinken
Copy link
Contributor Author

lvinken commented Aug 14, 2025

Hi Casper,

thanks for the quick feedback. Will fix the issues, which will take a few days due to other (higher priority) personal obligations.

With respect to timing, of course I'd prefer to have this in 4.6 but I understand the business need for feature freeze and respect any decision by the MuseScore team. I know a few people who are anxiously waiting for TablEdit import, who would be somewhat disappointed but I expect they can probably use the nightly builds whenever new features are merged into the master branch again.

Regards, Leon.

@cbjeukendrup
Copy link
Member

Hi Leon,
Yes, I understand that... Could you give me an idea of how complete the import is right now, and an estimated timeline for making it release-ready?
If that's within a week or so, then I might be able to convince everyone to include it in 4.6, given that this feature is so self-contained and can't possibly have unexpected effects on the rest of the application.

@Jojo-Schmitz
Copy link
Contributor

Jojo-Schmitz commented Aug 14, 2025

Well, first of all this needs to build properly (on GitHub CI) ;-)

@lvinken
Copy link
Contributor Author

lvinken commented Aug 14, 2025

Casper, Jojo,

fixing the build issues shouldn't be that hard, I would guess that probably fits within a week. Making the code release-ready depends a bit on a judgement call, as the TablEdit application and its file format are quite feature rich. With the amount of time I currently have available, to support most of the features will probably take me more than a year. Question would be "what would you consider a minimum viable product". I'll share a list of features supported and main missing features plus some realistic examples of imported TablEdit files.

Regards, Leon.

@lvinken
Copy link
Contributor Author

lvinken commented Aug 16, 2025

Build fixed, root cause of the allocation error was my mistake of putting const pointers in a std::vector, which is not allowed but is not reported by my compiler (Apple clang version 15.0.0 (clang-1500.3.9.4)) and causes no ill effect in my environment.

Unit tests fail on one file (TablEdit_Tests.tef_grace_1.tef) with a heap-buffer-overflow when calling mu::engraving::Part::setPlainLongName(muse::String const&), am looking into that.

Once all tests run OK, I'll look into the remaining comments.

@Jojo-Schmitz
Copy link
Contributor

Jojo-Schmitz commented Aug 16, 2025

Here's a fix for the MSVC compiler warnings

  • reg.: declaration of 'i' hides previous local declaration (C4465)
  • reg.: unreachable code (C4702)
  • reg.: conversion from 'size_t' to 'int', possible loss of data (C4267)
diff --git a/src/importexport/tabledit/internal/importtef.cpp b/src/importexport/tabledit/internal/importtef.cpp
index 4787031648..c640f4bb70 100644
--- a/src/importexport/tabledit/internal/importtef.cpp
+++ b/src/importexport/tabledit/internal/importtef.cpp
@@ -152,7 +152,7 @@ engraving::part_idx_t TablEdit::partIdx(size_t stringIdx, bool& ok) const

 // return total number of strings in previous parts

-int TablEdit::stringNumberPreviousParts(part_idx_t partIdx) const
+part_idx_t TablEdit::stringNumberPreviousParts(part_idx_t partIdx) const
 {
     part_idx_t result { 0 };
     for (part_idx_t i = 0; i < partIdx; ++i) {
@@ -318,7 +318,7 @@ void TablEdit::createContents()

     for (size_t part = 0; part < tefInstruments.size(); ++part) {
         LOGD("part %zu", part);
-        for (size_t voice = 0; voice < mu::engraving::VOICES; ++voice) {
+        for (int voice = 0; voice < mu::engraving::VOICES; ++voice) {
             LOGD("- voice %zu", voice);
             auto& voiceContent { voiceAllocators.at(part).voiceContent(voice) };
             TupletHandler tupletHandler;
@@ -865,13 +865,13 @@ void TablEdit::readTefInstruments()
         instrument.fClef = readUInt8();
         instrument.output = readUInt16();
         instrument.options = readUInt16();
-        for (uint16_t i = 0; i < 12; ++i) {
+        for (uint16_t j = 0; j < 12; ++j) {
             auto n = readUInt8();
-            instrument.tuning[i] = n;
+            instrument.tuning[j] = n;
         }
         // name is a zero-terminated utf8 string
         bool atEnd = false;
-        for (uint16_t i = 0; i < 36; ++i) {
+        for (uint16_t j = 0; j < 36; ++j) {
             auto c = readUInt8();
             if (c == 0) {
                 // stop appending, but continue reading
diff --git a/src/importexport/tabledit/internal/importtef.h b/src/importexport/tabledit/internal/importtef.h
index 39ff2dd2e8..0dd45a1d69 100644
--- a/src/importexport/tabledit/internal/importtef.h
+++ b/src/importexport/tabledit/internal/importtef.h
@@ -153,7 +153,7 @@ class TablEdit
     void createTitleFrame();
     void initializeVoiceAllocators(std::vector<VoiceAllocator>& allocators);
     engraving::part_idx_t partIdx(size_t stringIdx, bool& ok) const;
-    int stringNumberPreviousParts(engraving::part_idx_t partIdx) const;
+    engraving::part_idx_t stringNumberPreviousParts(engraving::part_idx_t partIdx) const;
     void readTefContents();
     void readTefHeader();
     void readTefInstruments();
diff --git a/src/importexport/tabledit/internal/voiceallocator.cpp b/src/importexport/tabledit/internal/voiceallocator.cpp
index 6b1e2100d0..0a4c0605b0 100644
--- a/src/importexport/tabledit/internal/voiceallocator.cpp
+++ b/src/importexport/tabledit/internal/voiceallocator.cpp
@@ -87,7 +87,6 @@ static int durationToInt(uint8_t duration) // TODO duplicated code ?
     case 28: return 7; //"16th double dotted";
     default: return 0; //"undefined";
     }
-    return 0; //"undefined";
 }

 int VoiceAllocator::stopPosition(const size_t voice)

@yonah-ag
Copy link

Re: Question would be "what would you consider a minimum viable product".

Maybe you could post that question here:
https://musescore.org/en/node/361417
since it's probably more frequented than this GitHub page.

There are some layout features in TablEdit that cannot be supported by MuseScore but most guitar notation should be OK even if playback effects do not work.

@cbjeukendrup
Copy link
Member

Regarding that question, I had the following thought: whether we can include it in a release depends more on the nature of the incompleteness than on its size. If files are imported without errors, generating valid MSCZ files, but just some amount of information is ignored, then it's perhaps already fine.
But if the import can't handle certain files yet, and produces invalid scores or starts reading data from wrong places, causing potential security issues, that would be problematic.

@yonah-ag
Copy link

That makes sense and might allow inclusion in 4.6. Basic score import without errors would get the ball rolling and provide a basis for forum users to comment on the next steps.

@lvinken
Copy link
Contributor Author

lvinken commented Aug 17, 2025

Replying to Jojo's fixes for the MSVC compiler warnings (thanks for looking into that, hadn't noticed it myself !):

  • for stringNumberPreviousParts() the actual issue is that result should be an int (it is not a part index but the number of strings which is an int everywhere)
  • making voice an int results in a warning when using Clang on macOS, I have copied the common solution in src/engraving (making voice a voice_idx_t)

Will push a new commit, let's see how that works out

@lvinken
Copy link
Contributor Author

lvinken commented Aug 17, 2025

Replying to Casper's message:

Regarding that question, I had the following thought: whether we can include it in a release depends more on the nature of the incompleteness than on its size. If files are imported without errors, generating valid MSCZ files, but just some amount of information is ignored, then it's perhaps already fine. But if the import can't handle certain files yet, and produces invalid scores or starts reading data from wrong places, causing potential security issues, that would be problematic.

Currently I am not aware of crashes or invalid MSCZ files being generated. The exception is the unit test in the Github environment. This issue does not reproduce for me (can only test on MacOS), so fixing it is not trivial. It does hint at some kind of memory corruption I have not yet been able to find.

TablEdit files containing guitar-like stringed instruments import correctly but some musical data will be missing as itis not yet imported (e.g. fingering, volume, lyrics, voltas).

@Jojo-Schmitz
Copy link
Contributor

Jojo-Schmitz commented Aug 17, 2025

Replying to Jojo's fixes for the MSVC compiler warnings (thanks for looking into that, hadn't noticed it myself !):

  • for stringNumberPreviousParts() the actual issue is that result should be an int (it is not a part index but the number of strings which is an int everywhere)
  • making voice an int results in a warning when using Clang on macOS, I have copied the common solution in src/engraving (making voice a voice_idx_t)

voice_idx_t and size_t are the same thing, so comes with the very same set of warnings, and'd require a static_cast<int>(voice) in 4 places, not so nice.

diff --git a/src/importexport/tabledit/internal/importtef.cpp b/src/importexport/tabledit/internal/importtef.cpp
index 9b770b5122..c18722f203 100644
--- a/src/importexport/tabledit/internal/importtef.cpp
+++ b/src/importexport/tabledit/internal/importtef.cpp
@@ -320,7 +320,7 @@ void TablEdit::createContents()
         LOGD("part %zu", part);
         for (voice_idx_t voice = 0; voice < mu::engraving::VOICES; ++voice) {
             LOGD("- voice %zu", voice);
-            auto& voiceContent { voiceAllocators.at(part).voiceContent(voice) };
+            auto& voiceContent { voiceAllocators.at(part).voiceContent(static_cast<int>(voice)) };
             TupletHandler tupletHandler;
             for (size_t k = 0; k < voiceContent.size(); ++k) {
                 LOGD("  - chord %zu", k);
@@ -374,7 +374,7 @@ void TablEdit::createContents()

                 if (firstNote->rest) {
                     LOGD("    - rest position %d string %d fret %d", firstNote->position, firstNote->string, firstNote->fret);
-                    addRest(segment, track, tDuration, length, toColor(voice));
+                    addRest(segment, track, tDuration, length, toColor((static_cast<int>(voice)));
                 } else {
                     LOGD("    - note(s) position %d string %d fret %d", firstNote->position, firstNote->string, firstNote->fret);
                     mu::engraving::Chord* chord { Factory::createChord(segment) };
@@ -396,11 +396,11 @@ void TablEdit::createContents()
                             int pitch = 96 - instrument.tuning.at(note->string - stringOffset - 1) + note->fret;
                             LOGD("      -> string %d fret %d pitch %d", note->string, note->fret, pitch);
                             // note TableEdit's strings start at 1, MuseScore's at 0
-                            addNoteToChord(chord, track, pitch, note->fret, note->string - 1, note->tie, toColor(voice));
+                            addNoteToChord(chord, track, pitch, note->fret, note->string - 1, note->tie, toColor((static_cast<int>(voice)));
                             if (note->hasGrace) {
                                 // todo fix magical constant 96 and code duplication
                                 int gracePitch = 96 - instrument.tuning.at(/* todo */ note->string - stringOffset - 1) + note->graceFret;
-                                addGraceNotesToChord(chord, gracePitch, note->graceFret, /* todo */ note->string - 1, toColor(voice));
+                                addGraceNotesToChord(chord, gracePitch, note->graceFret, /* todo */ note->string - 1, toColor((static_cast<int>(voice)));
                             }
                         }
                         tupletHandler.addCr(measure, chord);

Using an int instead doesn't have that issue and won't throw away any data, as we know that this variable contains 0-3 only.
Esp. as toColor() (3 of the 4 casts) doesn't use that argument at all:

static muse::draw::Color toColor(const int)
{
    // no debug: color notes black
    return muse::draw::Color::BLACK;
}

You could change

    const std::vector<std::vector<const TefNote*> >& voiceContent(int voice) const { return voiceContents.at(voice); }

to

    const std::vector<std::vector<const TefNote*> >& voiceContent(voice_idx_t voice) const { return voiceContents.at(voice); }

Doing the same fot toColor() too.

BTW: a rebase would get rid of 3 other compiler warnings (outside your code).

@lvinken
Copy link
Contributor Author

lvinken commented Aug 17, 2025

Rebased to current master (commit 7fc2f52 of Sun Aug 17 02:57:53 2025 +0200)

@Jojo-Schmitz
Copy link
Contributor

Jojo-Schmitz commented Aug 17, 2025

Still crashes in the unit tests, seems to be about
TablEdit_Tests_tef_grace_1_Test::TestBody() in /src/importexport/tabledit/tests/tabledit_tests.cpp line 70

@Jojo-Schmitz
Copy link
Contributor

Another crash, in TablEdit_Tests_tef_guitar_bass_Test::TestBody() ../src/importexport/tabledit/tests/tabledit_tests.cpp, line 80 and

@cbjeukendrup
Copy link
Member

@lvinken In order to reproduce the crash from CI, enable Address Sanitizer by passing the following option to the CMake configuration command:

-DMUSE_COMPILE_ASAN:BOOL=ON

Then rebuild the project.

@lvinken
Copy link
Contributor Author

lvinken commented Aug 17, 2025

Rebuilt with Address Sanitizer enabled, crash reproduces, will investigate.

@lvinken
Copy link
Contributor Author

lvinken commented Aug 19, 2025

My previous commit results in iex_tabledit_tests running correctly on macOS with Address Sanitizer enabled, but apparently not in the Github CI environment :-(. Will require some thinking to find out how to fix that ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ability to Import TablEdit Files
5 participants