Various tools for working with Ardour session files
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

change-tempo.py 4.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. #!/usr/bin/env python3
  2. # Copyright (C) 2018 nickolas360 <contact@nickolas360.com>
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. import math
  17. import sys
  18. DEBUG = False
  19. FORCE_SECURE = False
  20. try:
  21. import defusedxml.ElementTree as ElementTree
  22. except ImportError:
  23. if FORCE_SECURE:
  24. sys.exit("Error: The 'defusedxml' package must be installed.")
  25. import xml.etree.ElementTree as ElementTree
  26. class ExitError(Exception):
  27. def __init__(self, message):
  28. super().__init__(message)
  29. @property
  30. def message(self):
  31. return self.args[0]
  32. def read_xml(path):
  33. try:
  34. return ElementTree.parse(path)
  35. except FileNotFoundError as e:
  36. raise ExitError("File not found: '{}'".format(path)) from e
  37. def get_session(tree):
  38. return tree.getroot()
  39. def enforce_version(session):
  40. version_elem = session.find("ProgramVersion")
  41. if version_elem is None:
  42. raise ExitError("Could not get version from project file.")
  43. version_str = version_elem.attrib.get("modified-with")
  44. if version_str is None:
  45. version_str = version_elem.attrib.get("created-with")
  46. if version_str is None:
  47. raise ExitError("Could not get version from project file.")
  48. if not version_str.startswith("Ardour 6."):
  49. raise ExitError("Only Ardour 6 project files are supported.")
  50. def get_midi_routes(session):
  51. return session.findall(".//Route[@default-type='midi']")
  52. def get_automation_lists(route):
  53. return route.findall(".//AutomationList")
  54. def shift_events(automation_list, multiplier):
  55. events_elem = automation_list.find("events")
  56. if events_elem is None:
  57. return
  58. events = parse_events(events_elem.text)
  59. for i, (samples, value) in enumerate(events):
  60. samples = int(round(samples * multiplier))
  61. events[i] = (samples, value)
  62. events_elem.text = "".join(
  63. "{} {}\n".format(samples, value) for samples, value in events
  64. )
  65. def parse_events(events_text):
  66. events = []
  67. for line in events_text.splitlines():
  68. if not line:
  69. continue
  70. try:
  71. samples, value = line.split()
  72. samples = int(samples)
  73. except ValueError as e:
  74. raise ExitError(
  75. "Could not parse automation event line: {}".format(line),
  76. ) from e
  77. events.append((samples, value))
  78. return events
  79. def parse_bpm(bpm_str):
  80. try:
  81. bpm = float(bpm_str)
  82. except ValueError as e:
  83. raise ExitError("Invalid BPM: '{}'".format(bpm_str)) from e
  84. if bpm <= 0 or not math.isfinite(bpm):
  85. raise ExitError(
  86. "BPM must be a positive number: '{}'".format(bpm_str),
  87. )
  88. return bpm
  89. def run(argv):
  90. if len(argv) != 4:
  91. sys.exit(
  92. "Usage: change-tempo.py <ardour-project-file> <old-bpm> <new-bpm>",
  93. )
  94. path, old_bpm, new_bpm = argv[1:]
  95. old_bpm = parse_bpm(old_bpm)
  96. new_bpm = parse_bpm(new_bpm)
  97. tree = read_xml(path)
  98. session = get_session(tree)
  99. enforce_version(session)
  100. multiplier = old_bpm / new_bpm
  101. for route in get_midi_routes(session):
  102. for automation_list in get_automation_lists(route):
  103. shift_events(automation_list, multiplier)
  104. tree.write(path, xml_declaration=True, encoding="utf-8")
  105. def main(argv):
  106. try:
  107. run(argv)
  108. except ExitError as e:
  109. print(e.message, file=sys.stderr)
  110. if DEBUG:
  111. raise
  112. return 1
  113. return 0
  114. if __name__ == "__main__":
  115. sys.exit(main(sys.argv))