Capítulo 10.5

Scripts

Los scripts hacen de una vez lo que a mano llevaría horas: un acento en cada primer tiempo, quitar las dinámicas repetidas, transportar un pentagrama, cambiar el nombre de los instrumentos de cien partituras, comprobar las extensiones. Se escriben en Lua 5.4, el lenguaje de los plug-ins JW Lua y RGP Lua de Finale.

Un script ve toda la partitura (pentagramas, compases, notas, signos, textos, maquetación) y puede cambiarla. Trabaja sobre una copia: si termina, todos sus cambios se convierten en un solo paso de Deshacer; si se detiene por un error, la partitura queda como estaba. Los scripts se ejecutan en un entorno cerrado: no pueden leer ni escribir archivos, usar la red ni abrir programas.

El menú Scripts

Cada archivo .lua de la carpeta de scripts es una opción del menú Scripts; las subcarpetas son submenús. La primera vez, Maestro copia allí algunos ejemplos, en la carpeta Ejemplos: ábrelos con un editor de texto para ver cómo están hechos. Algunos rehacen los scripts más usados de la colección de RGP Lua para Finale (conservar una nota de los acordes, duplicar a la octava, intercambiar dos capas, quitar las articulaciones de los silencios, contar las duraciones).

  • Scripts → Abrir la carpeta de scripts la muestra en el Finder o en el Explorador de archivos (en el Mac es ~/Library/Application Support/Maestro/Script, en Windows %LOCALAPPDATA%\Maestro\Script). Un archivo nuevo o cambiado vale enseguida, sin reiniciar.
  • Un script trabaja sobre la partitura en primer plano: sobre la selección, si el script la usa, o sobre todo.
  • Ejecutar de nuevo (⌥⌘U, en Windows Ctrl+Alt+U) repite el último script con las mismas respuestas a los parámetros.
  • Detener el script para un script que tarda demasiado; a los dos minutos se para de todas formas.
  • Ejecutar un script… abre cualquier archivo .lua, también fuera de la carpeta.
  • Mientras el script se ejecuta, la partitura queda libre; pero si la cambias entretanto, los cambios del script no se aplican (la consola lo dice), así nunca pierdes lo que has escrito.

En el iPad, el botón Scripts del atril ejecuta los scripts de la carpeta Script de Maestro (en la app Archivos: En mi iPad → Maestro → Script), por ejemplo para transportar una obra mientras la lees; los cambios valen para la lectura y el archivo no cambia.

La consola de scripts

Scripts → Consola de scripts muestra lo que los scripts escriben con print, los mensajes de Maestro y los errores, con el archivo y la línea («Acentos.lua:12: Duración no válida…»). Se abre sola tras un error.

En la línea de abajo puedes escribir Lua para la partitura en primer plano y pulsar Intro: una expresión muestra su valor (score.title, #score.staves), una instrucción se ejecuta (score.title = "Sonata"), con un paso de Deshacer. Las flechas ↑ y ↓ recuperan las líneas anteriores.

Escribir un script

-- Un acento en la primera nota de cada compás de la flauta,
-- y las notas por encima del do6 una octava más abajo
local flauta = score.staves:find("Flauta")
for _, compas in ipairs(flauta.measures) do
  local primera = compas:layer(1)[1]
  if primera and primera.isNote then primera:add("accent") end
end
for e in flauta:entries() do
  for _, nota in ipairs(e.notes) do
    if nota.midi > 84 then nota:transpose("-P8") end
  end
end
  • Los números empiezan en 1, como en Lua y como los números de compás: score.staves[1], compas:layer(1).
  • Las propiedades se leen y escriben con punto (e.duration = "8"), los métodos se llaman con dos puntos (e:add("accent")).
  • Las listas (score.staves, pentagrama.measures, compas:layer(1), e.notes, compas.dynamics…) se cuentan con # y se recorren con ipairs; los iteradores score:entries(), pentagrama:entries(), compas:entries() dan todas las notas, acordes y silencios.
Objeto Qué es Algunas propiedades y métodos
score la partitura title, composer, tempo, staves, measures, pageFormat; insertMeasures, deleteMeasures, setKey, setTime, transpose, addStaff
pentagrama score.staves[n] name, abbreviatedName, clef, measures; setClef, move, delete
compás pentagrama.measures[n] number, clef, dynamics, hairpins, texts, chords; layer(n), append, clear, fillWithRests
compás de la partitura score.measures[n] time, key; setTime, setKey
nota, acorde o silencio compas:layer(1)[n] duration, isRest, isChord, tick, tied, articulations, notes; add, remove, transpose, setNotes, delete
altura e.notes[n] name, step, alter, octave, midi; transpose, delete
signos compas.dynamics[n], score.slurs[n]… los campos del signo (marking, tick…); delete; en las listas add({ … })

Las duraciones se escriben "w", "h", "q", "8", "16", "32" con los puntillos ("q."); las alturas "C4" (do central), "F#5", "Bb3" o también "Sol4"; los intervalos "P8", "-m3", "M2", "A4"; las articulaciones "accent", "staccato", "tenuto", "fermata"….

Además de las propiedades de la tabla, cada objeto llega a cada campo de la partitura con el nombre que tiene en el archivo .maestro: e.hidden = true, e:set("pitches.1.notehead", "diamond"), pentagrama:raw() (todo el objeto como tabla) y :replace(tabla), :fields(). Un nombre equivocado da un error en vez de pasar en silencio. La lista completa la da el propio script: maestro.schema() y maestro.schema("entry").

Recorrer la partitura

Las funciones eachentry, eachcell, eachstaff y each (las mismas que en RGP Lua) recorren una región: sin argumentos, la selección o, si no la hay, toda la partitura.

-- Un staccato en todas las corcheas de la selección (o de la partitura)
for e in eachentry() do
  if e.isNote and e.duration == "8" then e:add("staccato") end
end
Función Qué da
eachentry(region, capa) notas, acordes y silencios, pentagrama por pentagrama y compás por compás; capa de 1 a 4 (todas por omisión)
eachcell(region) cada compás de cada pentagrama: for compas, pentagrama in eachcell() do … end
eachstaff(region) los pentagramas
each(lista), eachbackwards(lista) los elementos de una lista, o las notas de un acorde (each(e)); hacia atrás se puede borrar mientras se recorre
pairsbykeys(tabla) los pares de una tabla en orden de clave

La región puede ser selection (solo la selección: nada si no la hay), score, un pentagrama, un compás (score.staves[1].measures[4] o score.measures[4]) o una tabla como { staves = {1, 2}, measures = {5, 8} }: un número, o el primero y el último; lo que falta vale todo. También existe eachentrysaved, para quien viene de Finale, pero es igual que eachentry: en Maestro los cambios valen enseguida.

Los comandos del editor

editor.perform hace lo que harías a mano con las paletas de Articulaciones, Expresiones, Líneas, Grupos irregulares, Repeticiones y Maquetación: ligaduras, reguladores, líneas de octava, textos, casillas, saltos de sistema… Actúa sobre una región, como una selección: por defecto la selección, o una entrada, una lista de entradas, un compás, un pentagrama o { staves = …, measures = … }. Las preguntas de la ventana del comando se responden con las opciones.

editor.perform("slur", { staves = 1, measures = {1, 2} })            -- ligadura de la primera a la última nota
editor.perform("crescendo", score.staves[1].measures[5])
editor.perform("ending", { measures = 8, numbers = {1} })            -- primera casilla
editor.perform("tuplet", e, { ratio = "5:4" })                      -- quintillo desde la entrada e
editor.perform("text", compas, { text = "dolce", kind = "expression" })
editor.perform("systemBreak", { measures = 9 })                      -- sistema nuevo desde el compás 9

editor.commands() da la lista de comandos con sus opciones. Las articulaciones se ponen o se quitan, como en la paleta. Si el editor rechaza el comando («Selecciona al menos dos notas…»), el script se detiene con un error en esa línea; también estos comandos quedan en el único paso de Deshacer del script. Cada comando vuelve a maquetar la partitura: en bucles largos conviene un comando sobre una región grande y no uno por nota.

Nombre, atajo y parámetros

Un bloque de metadatos opcional al principio del script da el nombre en el menú (en varios idiomas), una descripción, un atajo y los parámetros que se piden antes de empezar:

--[[ maestro
name = { es = "Transponer", en = "Transpose" }
description = { es = "Transpone la selección el intervalo elegido." }
shortcut = "mod+alt+shift+Y"   -- mod = ⌘ en el Mac, Ctrl en Windows; o "F9"
needsSelection = true
params = {
  { name = "interval", type = "text", default = "M2", label = { es = "Intervalo" } },
  { name = "veces", type = "number", default = 1, min = 1, max = 4 },
}
]]
for e in selection.entries() do e:transpose(params.interval) end

Maestro muestra un formulario con los parámetros (la vez siguiente propone las respuestas anteriores) y el script los encuentra en params. Los tipos son number, text, boolean y choice (con choices = { … }). Un atajo que Maestro ya usa no se asigna. require("nombre") carga los módulos de la carpeta lib de la carpeta de scripts.

Sobre una carpeta entera

Scripts → Ejecutar sobre una carpeta… aplica un script a todas las partituras de una carpeta (también en las subcarpetas): Maestro, Finale, MusicXML, MuseScore y MIDI. En el formulario eliges los parámetros del script y el resultado: solo el informe, o los archivos guardados en otra carpeta como documentos de Maestro, MusicXML, MusicXML comprimido, Finale (.musx) o MIDI. Los archivos originales no cambian nunca. La consola muestra el resultado archivo por archivo.

Desde el terminal: maestro-script

El comando maestro-script hace lo mismo sin ventanas, para procesos por lotes y para otros programas:

maestro-script run Acentos.lua Partituras/ --recursive --out Convertidas/ --format musicxml
maestro-script eval 'score.title' Partituras/
maestro-script run Transponer.lua obra.musx -p interval=-M2 --out Salida/ --json
maestro-script help

En el Mac está dentro de Maestro: Scripts → Instalar el comando maestro-script… lo deja disponible en el Terminal. En Windows es maestro-script.exe en la carpeta de Maestro (Scripts → El comando maestro-script… dice dónde). No está en la versión del Mac App Store.

Con un asistente (Claude y otros)

maestro-script mcp es un servidor MCP: un asistente como Claude puede abrir tus partituras, escribir y ejecutar scripts, mirar las páginas y guardar el resultado. Solo lee y escribe en las carpetas que le indicas con --root; nunca sobrescribe el original ni, sin --allow-overwrite, un archivo que ya existe.

Con Claude Code, en el Mac:

claude mcp add maestro -- /Applications/Maestro.app/Contents/MacOS/maestro-script mcp --root ~/Partituras

Con Claude Desktop, en el archivo de configuración de los servidores MCP:

{
  "mcpServers": {
    "maestro": {
      "command": "/Applications/Maestro.app/Contents/MacOS/maestro-script",
      "args": ["mcp", "--root", "/Users/yo/Partituras"]
    }
  }
}

En Windows el comando es %LOCALAPPDATA%\Programs\Maestro\maestro-script.exe. Después basta con pedir: «abre Minueto.musx, pon un acento en cada primer tiempo de la flauta, enséñame la primera página y guárdalo como Minueto-acentos.maestro».

Sobre las partituras abiertas en Maestro

Un asistente también puede trabajar sobre las partituras abiertas en la app mientras las miras. Está desactivado de serie: actívalo con Scripts → Permitir que los asistentes modifiquen las partituras abiertas y conecta el asistente con maestro-script mcp --app; con Claude Code, en el Mac:

claude mcp add maestro-app -- /Applications/Maestro.app/Contents/MacOS/maestro-script mcp --app

El asistente ve las partituras abiertas y tu selección; cada uno de sus scripts es un paso de Deshacer llamado «Asistente» y aparece en la consola de scripts; los archivos los guardas tú. La conexión se queda en tu ordenador y tu usuario, sin red.

Escribir scripts con un editor

La carpeta de scripts contiene también la descripción de la interfaz para los editores (.maestro/maestro.d.lua y .luarc.json): ábrela en Visual Studio Code con la extensión de Lua para tener el autocompletado de propiedades y métodos y las comprobaciones mientras escribes.

Los datos originales de Finale

Una partitura abierta desde un archivo de Finale (.musx o .mus) conserva, mientras sigue abierta, los datos originales del archivo: los scripts los leen con finale, con los nombres del EnigmaXML (los mismos de musxdom y del PDK de Finale). Sirven para recuperar lo que la importación no trae a Maestro, o para comparar.

local ms = finale.others("measSpec", 1)[1]          -- el compás 1 tal como lo escribe Finale
print(ms.beats, ms.divbeat, ms.width)
for e in score:entries() do
  local n = finale.entnum(e)                         -- la entrada de Finale de la que viene
  if n then
    local original = finale.entry(n)
    -- … por ejemplo recuperar algo que la importación no lee
  end
end
  • finale.others(tag, cmper, inci), finale.details(tag, cmper1, cmper2, inci), finale.texts(tag, numero): los registros (los argumentos tras el nombre son filtros opcionales); finale.entry(entnum), finale.header(), finale.options(tag), finale.tags() (los nombres presentes).
  • Un registro es una tabla: _tag es el nombre, los atributos y los elementos son campos; un elemento vacío vale true, uno repetido es una secuencia (finale.list(x) da siempre una secuencia).
  • finale.entryFor(entnum) y finale.entnum(entrada), finale.staffFor(cmper) y finale.staffCmper(pentagrama) enlazan los objetos de Finale con los de Maestro.
  • Es de solo lectura y no se guarda: tras Guardar como .maestro y volver a abrir, finale ya no está. En los .mus contiene los registros que Maestro sabe leer.

Para quien viene de RGP Lua

El lenguaje es el mismo, los objetos son los de Maestro:

RGP Lua (Finale) Maestro
plugindef() el bloque --[[ maestro … ]] al principio del script
finenv.Region() selection; sin región, las funciones para recorrer usan la selección o toda la partitura
for e in eachentrysaved(finenv.Region()) for e in eachentry() (también eachentrysaved, eachcell, eachstaff, each, pairsbykeys)
finale.FCStaves, FCStaff score.staves, score.staves[n]
finale.FCMeasures, FCMeasure score.measures[n] (compás y armadura), pentagrama.measures[n] (la música)
FCNoteEntryLayer, FCNoteEntry compas:layer(n), compas:layer(n)[i]
FCNote e.notes[i]
entry.Duration (EDU, negra 1024) e.duration ("q") o e.ticks (negra 960)
FCExpression, FCSmartShape compas.dynamics, compas.texts, score.slurs, compas.hairpins…
entry:Save(), measure:Save() no hace falta: los cambios valen enseguida (y se deshacen todos juntos)