# Manipulate lyrics verses in a MuseScore 2 .mscx file # # Created by: .m.i.r.o. # Last modified: # 19/05/2015 # 20/06/2015 small bug fixes # 11/01/2016 added some comments and clarifications, added as synomym for # 25/02/2016 idea: apply to seleced staff/voice; defaults: s[taff]=all, v[oice]=all # 01/05/2017 implemented staffFrom / staffTo (*TODO* testing and documentation) # 09/07/2018 # - added check if the input is a uncompressed text/xml stream - abort if failed # - added reset of manual position adjustment for copied/shifted syllables # (they were showing at the previous position even though technically they have moved, # Ctrl-R/reset position within MuseScore would solve it as well). # # USAGE: # # gawk (-v var=value)+ -f _lyricsOps.awk < yourScore.mscx > yourScoreModified.mscx # # -v var=value can be used multiple times to define parameters. # WARNING: awk variable names are case sensitive! # # verse range or source/target: # verseFrom= (synonyms: fromVerse= vFrom= source= sourceVerse=), default: 1 # verseTo= (synonyms: toVerse= vTo= target= targetVerse=) , default: # # mode= (synonyms: m= action= a= operation= ops= op= o=), default: insert # insert (i) verseFrom -> verseTo # insert one empty or a range of empty verses before # specifies the last empty verse to be inserted # all subsequent verses (starting with ) are preserved and pushed down # e.g. -v verseFrom=2 -v verseTo=3 # verse 1 stays unchanged # verse 2 & 3 will become empty and # verse 4 will have content of old verse 2 # verse 5 will gets the content of old verse 3 etc. # default is := (insert one verse) # delete (d) verseFrom -> verseTo # delete one or more verses starting with and ending with (inclusive) # all subsequent verses will move up, e.g. -v versFrom=2 -v verseTo=3 # verse 1 stays unchanged # verse 2 & 3 gets deleted # verse 4 will now become verse 2 # verse 5 will now become verse 3 etc. # default is := (delete one verse) # swap (s) verseFrom <-> verseTo # copy (c) verseFrom -> verseTo # copy all the lyrics from verseFrom to verseTo # if target verse already exists it will be overwritten # # measure range: # measureFrom= (synonyms: mFrom= fromMeasure= from=), default: 1 # measureTo= (synonyms: mTo= toMeasure= to=) , default: 999999 # # # NOTES: 1. unfortunately -v needs to be specified multiple times (before each parameter) # 2. actions can be abbreviated to the first letter e.g.: i = insert # 3. parameter names are case sensitive (e.g. vFrom is valid, vfrom is NOT valid) # 4. parameters may be specified in any order; synonyms can be mixed freely # # Examples: # # Insert empty verse before verse 1 # -v mode=insert # -v m=insert -v verseFrom=1 -v verseTo=1 # # Insert 2 empty verses before verse 2 # -v m=i -v vFrom=2 -v vTo=3 # # Delete verses 3-4: # -v m=d -v vFrom=3 -v verseTo=4 # # Swap verse 2 with verse 5: # -v m=swap -v vFrom=2 -v vTo=5 # -v action=s -v source=5 -v target=2 # # Copy verse 4 to (and overwrite) verse 2 # -v operation=copy -v source=4 -v target=2 # -v ops=c -v verseFrom=4 -v vTo=2 # # Insert 2 empty verse before verse 2 but only from measure 23 onwards # -v m=i -v vFrom=2 -v vTo=3 -v mFrom=23 # # Copy verse 1 to (and overwrite) verse 2 for measures 10-20 (inclusive) # -v m=c -v source=1 -v target=2 -v from=10 -v to=20 # # WARNING: measure counts start from 1; irregular measures are counted as well; # this is how MuseScore shows the measure number in status line # (adjustments to measure numbers and/or section breaks are NOT considered) # BEGIN { # WARNING: awk variable names are case sensitive! # Parse command line parameters # (this could be a lot shorter but this way it should be more user-friendly) # either of the listed will work: action= a= operation= ops= op= o= mode= m= if (action == "") { action = a } if (action == "") { action = operation } if (action == "") { action = ops } if (action == "") { action = op } if (action == "") { action = o } if (action == "") { action = mode } if (action == "") { action = m } action = tolower(action) switch (action) { case "copy": case "c": action = "copy"; break; case "delete": case "d": action = "delete"; break; case "swap": case "s": action = "swap"; break; case "insert": case "i": action = "insert"; break; default: print "ABORTED: unknown operation requested - \"" action "\"" > "/dev/stderr" exit; break; } if (verseFrom == "") { verseFrom = fromVerse } if (verseFrom == "") { verseFrom = vFrom } if (verseFrom == "") { verseFrom = source } if (verseFrom == "") { verseFrom = sourceVerse } if (verseTo == "") { verseTo = toVerse } if (verseTo == "") { verseTo = vTo } if (verseTo == "") { verseTo = target } if (verseTo == "") { verseTo = targetVerse } if (verseTo == "") { verseTo = verseFrom } # verses are numbered internally 0--(n-1) if (--verseFrom < 0) { verseFrom = 0 } if (--verseTo < 0) { verseTo = 0 } # Insert/Delete work on a range - ensure that (verseFrom <= verseTo). # Swap - the order does not matter. # Copy - it is important to be able to specify source > target (verseFrom > verseTo). if ( (verseFrom > verseTo) && ((action == "insert") || (action == "delete") ) ) { no = verseTo verseTo = verseFrom verseFrom = no } # we number the measures 1 (for each staff) because actually they are not really numberered properly if (measureFrom == "") { measureFrom = mFrom } if (measureFrom == "") { measureFrom = fromMeasure } if (measureFrom == "") { measureFrom = from } if (measureTo == "") { measureTo = mTo } if (measureTo == "") { measureTo = toMeasure } if (measureTo == "") { measureTo = to } if (measureFrom == "") { measureFrom = 1 } if (measureTo == "") { measureTo = 999999 } # convert to (whole) number: measureFrom += 0 measureTo += 0 # Ensure measure range makes sense if (measureFrom > measureTo) { no = measureTo measureTo = measureFrom measureFrom = no } # staff if (staffFrom == "") { staffFrom = sFrom } if (staffFrom == "") { staffFrom = fromStaff } if (staffTo == "") { staffTo = toStaff} if (staffTo == "") { staffTo = sTo } if (staffFrom == "") { staffFrom = 1 } if (staffTo == "") { staffTo = 999999 } # convert to (whole) number: staffFrom += 0 staffTo += 0 # if (staffFrom < 1) { staffFrom = 1 } if (staffFrom > staffTo) { no = staffTo staffTo = staffFrom staffFrom = no } print "Staff: " staffFrom "-" staffTo ", " "Measure: " measureFrom "-" measureTo ", " toupper(action) " verse: " verseFrom+1 " -> " verseTo+1 > "/dev/stderr" lyrics = 0 no = 0 s = 1 m = 0 } (NR == 1) { # check if the stream is a text xml-like file if (match($0,/^ *<\?xml.*/) == 0) { print "ERROR: Input is not a text/xml stream. Make sure you're feeding in an uncompressed *.MSCX file." > "/dev/stderr" print "WARNING: Output stream truncated." > "/dev/stderr" exit; } } function sLyricsVerseNo(newNo) { # the original verse 0 does not have [0-9]+ line - add it manually where appropriate if (match(sLyrics,/.*[0-9]+<\/no>.*/) == 0) { sLyrics = gensub(/(^ +)()(.*)/,"\\1\\2\n\\1 0\\3" ,"g",sLyrics) } # replace verse number with a new value (newNo) sLyrics = gensub(/(.*)([0-9]+)(<\/no>.*)/,"\\1" newNo "\\3","g",sLyrics) # optionally remove 0 line sLyrics = gensub(/(.*)(\n +0<\/no>)(.*)/,"\\1\\3","g",sLyrics) #optionally reset/remove manual position adjustment sLyrics = gensub(/(.*)(\n +)(.*)/,"\\1\\3","g",sLyrics) } /^ +/ { m = 0 s = gensub(/(.*.*)/,"\\2","g") +0 } /^ +/ { ++m } # ---------------------------------------------------------------------------------------- # # The basic idea is to buffer the whole block and once the end of the block () # gets dectected, the requested operation will be executed. # /^ +/ { no = 0; sLyrics = ""; lyrics = 1 } /^ +[0-9]+<\/no>/ && (lyrics > 0) { no = gensub(/(.*)(.*)(<\/no>.*)/,"\\2","g") +0 } /^ +<\/Lyrics>/ { if (m >= measureFrom && m <= measureTo && s >= staffFrom && s <=staffTo) { if (action == "insert") { if (no >= verseFrom) { # move all subsequent verses one or more lines down sLyricsVerseNo( no + (verseTo - verseFrom +1) ) } } else if (action == "delete") { if (no >= verseFrom && no <= verseTo) { # delete any lyrics in the given range lyrics = 0; # sLyrics = "" next } if (no > verseTo) { # move all subsequent verses one or more lines up sLyricsVerseNo( no - (verseTo - verseFrom +1) ) } } else if (action == "copy") { # Note that gawk reads text line by line and intially the verses are numbered sequentially # but the output may not be sequential, # Example: # copy 4 -> 2; write verse #1, delete #2, write #3 followed by #4 and #4 again(as #2); sequence: #1, #3, #4, #2 # copy 2 -> 4; write verses #1, #2, #2 (as #4), #3; delete #4; sequence: #1, #2, #4, #3 # Once the file will be saved by MuseScore, the verses become sequential again. if (verseTo == no) { # delete the old verse lyrics = 0; # sLyrics = "" next } if (verseFrom == no) { # preserve the copied content (verseFrom) print sLyrics print $0 # save content with new verse number (verseTo) sLyricsVerseNo( verseTo ) } } else if (action == "swap") { # Note that swapping does not change the position of the text in the file. It just updates the verse numbers. # Once the file will be saved by MuseScore, the verses become sequential again. if (verseFrom == no || verseTo == no) { sLyricsVerseNo( no == verseFrom ? verseTo : verseFrom ) } } } print sLyrics lyrics = 0 } (lyrics > 0) { # store the whole tag (with all sub-tags) to a buffer - don't print the line yet if (sLyrics != "") { sLyrics = sLyrics "\n" } sLyrics = sLyrics $0 next } { print $0 } END {}