Audio plugin host https://kx.studio/carla
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.

160 lines
5.4KB

  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import math
  4. import lilv
  5. import sys
  6. import wave
  7. import numpy
  8. class WavFile(object):
  9. """Helper class for accessing wav file data. Should work on the most common
  10. formats (8 bit unsigned, 16 bit signed, 32 bit signed). Audio data is
  11. converted to float32."""
  12. # (struct format code, is_signedtype) for each sample width:
  13. WAV_SPECS = {
  14. 1: ("B", False),
  15. 2: ("h", True),
  16. 4: ("l", True),
  17. }
  18. def __init__(self, wav_in_path):
  19. self.wav_in = wave.open(wav_in_path, 'r')
  20. self.framerate = self.wav_in.getframerate()
  21. self.nframes = self.wav_in.getnframes()
  22. self.nchannels = self.wav_in.getnchannels()
  23. self.sampwidth = self.wav_in.getsampwidth()
  24. wav_spec = self.WAV_SPECS[self.sampwidth]
  25. self.struct_fmt_code, self.signed = wav_spec
  26. self.range = 2 ** (8*self.sampwidth)
  27. def read(self):
  28. """Read data from an open wav file. Return a list of channels, where each
  29. channel is a list of floats."""
  30. raw_bytes = self.wav_in.readframes(self.nframes)
  31. struct_fmt = "%u%s" % (len(raw_bytes) / self.sampwidth, self.struct_fmt_code)
  32. data = wave.struct.unpack(struct_fmt, raw_bytes)
  33. if self.signed:
  34. data = [i / float(self.range/2) for i in data]
  35. else:
  36. data = [(i - float(range/2)) / float(range/2) for i in data]
  37. channels = []
  38. for i in range(self.nchannels):
  39. channels.append([data[j] for j in range(0, len(data), self.nchannels) ])
  40. return channels
  41. def close(self):
  42. self.wav_in.close()
  43. def main():
  44. # Read command line arguments
  45. if len(sys.argv) != 4:
  46. print('USAGE: lv2_apply.py PLUGIN_URI INPUT_WAV OUTPUT_WAV')
  47. sys.exit(1)
  48. # Initialise Lilv
  49. world = lilv.World()
  50. ns = world.ns
  51. world.load_all()
  52. plugin_uri = sys.argv[1]
  53. wav_in_path = sys.argv[2]
  54. wav_out_path = sys.argv[3]
  55. # Find plugin
  56. plugin_uri_node = world.new_uri(plugin_uri)
  57. plugins = world.get_all_plugins()
  58. if plugin_uri_node not in plugins:
  59. print("Unknown plugin `%s'" % plugin_uri)
  60. sys.exit(1)
  61. plugin = plugins[plugin_uri_node]
  62. n_audio_in = plugin.get_num_ports_of_class(ns.lv2.InputPort, ns.lv2.AudioPort)
  63. n_audio_out = plugin.get_num_ports_of_class(ns.lv2.OutputPort, ns.lv2.AudioPort)
  64. if n_audio_out == 0:
  65. print("Plugin has no audio outputs\n")
  66. sys.exit(1)
  67. # Open input file
  68. try:
  69. wav_in = WavFile(wav_in_path)
  70. except:
  71. print("Failed to open input `%s'\n" % wav_in_path)
  72. sys.exit(1)
  73. if wav_in.nchannels != n_audio_in:
  74. print("Input has %d channels, but plugin has %d audio inputs\n" % (
  75. wav_in.nchannels, n_audio_in))
  76. sys.exit(1)
  77. # Open output file
  78. wav_out = wave.open(wav_out_path, 'w')
  79. if not wav_out:
  80. print("Failed to open output `%s'\n" % wav_out_path)
  81. sys.exit(1)
  82. # Set output file to same format as input (except possibly nchannels)
  83. wav_out.setparams(wav_in.wav_in.getparams())
  84. wav_out.setnchannels(n_audio_out)
  85. print('%s => %s => %s @ %d Hz'
  86. % (wav_in_path, plugin.get_name(), wav_out_path, wav_in.framerate))
  87. instance = lilv.Instance(plugin, wav_in.framerate)
  88. channels = wav_in.read()
  89. wav_in.close()
  90. # Connect all ports to buffers. NB if we fail to connect any buffer, lilv
  91. # will segfault.
  92. audio_input_buffers = []
  93. audio_output_buffers = []
  94. control_input_buffers = []
  95. control_output_buffers = []
  96. for index in range(plugin.get_num_ports()):
  97. port = plugin.get_port_by_index(index)
  98. if port.is_a(ns.lv2.InputPort):
  99. if port.is_a(ns.lv2.AudioPort):
  100. audio_input_buffers.append(numpy.array(channels[len(audio_input_buffers)], numpy.float32))
  101. instance.connect_port(index, audio_input_buffers[-1])
  102. elif port.is_a(ns.lv2.ControlPort):
  103. default = float(port.get(ns.lv2.default))
  104. control_input_buffers.append(numpy.array([default], numpy.float32))
  105. instance.connect_port(index, control_input_buffers[-1])
  106. else:
  107. raise ValueError("Unhandled port type")
  108. elif port.is_a(ns.lv2.OutputPort):
  109. if port.is_a(ns.lv2.AudioPort):
  110. audio_output_buffers.append(numpy.array([0] * wav_in.nframes, numpy.float32))
  111. instance.connect_port(index, audio_output_buffers[-1])
  112. elif port.is_a(ns.lv2.ControlPort):
  113. control_output_buffers.append(numpy.array([0], numpy.float32))
  114. instance.connect_port(index, control_output_buffers[-1])
  115. else:
  116. raise ValueError("Unhandled port type")
  117. # Run the plugin:
  118. instance.run(wav_in.nframes)
  119. # Interleave output buffers:
  120. data = numpy.dstack(audio_output_buffers).flatten()
  121. # Return to original int range:
  122. if wav_in.signed:
  123. data = data * float(wav_in.range / 2)
  124. else:
  125. data = (data + 1) * float(wav_in.range/2)
  126. # Write output file in chunks to stop memory usage getting out of hand:
  127. CHUNK_SIZE = 8192
  128. for chunk in numpy.array_split(data, CHUNK_SIZE):
  129. wav_out.writeframes(wave.struct.pack("%u%s" % (len(chunk), wav_in.struct_fmt_code), *chunk.astype(int)))
  130. wav_out.close()
  131. if __name__ == "__main__":
  132. main()