audio.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import subprocess
  2. class Audio:
  3. def __init__(self):
  4. self.sox_process = None
  5. def kill_sox(self, timeout=1):
  6. if self.sox_process is not None:
  7. self.sox_process.terminate()
  8. try:
  9. self.sox_process.wait(timeout=timeout)
  10. except subprocess.TimeoutExpired:
  11. self.sox_process.kill()
  12. self.sox_process.wait(timeout=timeout)
  13. self.sox_process = None
  14. # trying a lower buffer size
  15. def run_sox(self, scale, preset, buffer=20):
  16. '''
  17. Builds and returns a sox command from a preset object
  18. '''
  19. buffer = 17
  20. multiplier = 100
  21. command_effects = []
  22. command_effects += ["pitch", str(scale * multiplier)]
  23. # Volume boosting
  24. if preset.volume_boost != None:
  25. command_effects += ["vol", str(preset.volume_boost) + "dB"]
  26. else:
  27. # Fix a bug where SoX uses last given volumne
  28. command_effects += ["vol", "0"]
  29. # Downsampling
  30. if preset.downsample_amount != None:
  31. command_effects += ["downsample", str(preset.downsample_amount)]
  32. else:
  33. # Append downsample of 1 to fix a bug where the downsample isn't being reverted
  34. # when we disable the effect with it on.
  35. command_effects += ["downsample", "1"]
  36. command = ["sox", "--buffer", str(buffer), "-q", "-t", "pulseaudio", "default", "-t", "pulseaudio", "Lyrebird-Output"] + command_effects
  37. self.sox_process = subprocess.Popen(command)
  38. def get_sink_name(self, tuple):
  39. if tuple[0] == "sink_name":
  40. return tuple[1]
  41. elif tuple[0] == "source_name":
  42. return tuple[1]
  43. else:
  44. return None
  45. def load_pa_modules(self):
  46. self.null_sink = subprocess.check_call(
  47. 'pactl load-module module-null-sink sink_name=Lyrebird-Output node.description="Lyrebird Output"'.split(' ')
  48. )
  49. self.remap_sink = subprocess.check_call(
  50. 'pactl load-module module-remap-source source_name=Lyrebird-Input master=Lyrebird-Output.monitor node.description="Lyrebird Virtual Input"'\
  51. .split(' ')
  52. )
  53. def get_pactl_modules(self):
  54. '''
  55. Parses `pactl info short` into tuples containing the module ID,
  56. the module type and the attributes of the module. It is designed
  57. only for named modules and as such junk data may be included in
  58. the returned list.
  59. Returns an array of tuples that take the form:
  60. (module_id (str), module_type (str), attributes (attribute tuples))
  61. The attribute tuples:
  62. (key (str), value (str))
  63. An example output might look like:
  64. [
  65. ( '30', 'module-null-sink', [('sink_name', 'Lyrebird-Output')] ),
  66. ( '31', 'module-remap-source', [('source_name', 'Lyrebird-Input'), ('master', 'Lyrebird-Output.monitor')] )
  67. ]
  68. '''
  69. pactl_list = subprocess.run(["pactl", "list", "short"], capture_output=True, encoding="utf8")
  70. lines = pactl_list.stdout
  71. data = []
  72. split_lines = lines.split("\n")
  73. for line in split_lines:
  74. info = line.split("\t")
  75. if len(info) <= 2:
  76. continue
  77. if info[2] and len(info[2]) > 0:
  78. key_values = list(map(lambda key_value: tuple(key_value.split("=")), info[2].split(" ")))
  79. data.append((info[0], info[1], key_values))
  80. else:
  81. data.append((info[0], info[1], []))
  82. return data
  83. def unload_pa_modules(self):
  84. '''
  85. Unloads all Lyrebird null sinks.
  86. '''
  87. modules = self.get_pactl_modules()
  88. lyrebird_module_ids = []
  89. for module in modules:
  90. if len(module) < 3:
  91. continue;
  92. if len(module[2]) < 1:
  93. continue;
  94. if module[1] == "module-null-sink":
  95. sink_name = self.get_sink_name(module[2][0])
  96. if sink_name == "Lyrebird-Output":
  97. lyrebird_module_ids.append(module[0])
  98. elif module[1] == "module-remap-source":
  99. sink_name = self.get_sink_name(module[2][0])
  100. if sink_name == "Lyrebird-Input":
  101. lyrebird_module_ids.append(module[0])
  102. for id in lyrebird_module_ids:
  103. subprocess.run(["pactl", "unload-module", str(id)])