mainwindow.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. #!/bin/python
  2. import gi
  3. import subprocess
  4. gi.require_version('Gtk', '3.0')
  5. from gi.repository import Gtk, Gdk, GdkPixbuf
  6. # Core imports
  7. import app.core.presets as presets
  8. import app.core.state as state
  9. import app.core.config as config
  10. import app.core.utils as utils
  11. from app.core.presets import Preset
  12. # Multiplier for pitch shifting
  13. sox_multiplier = 100
  14. class MainWindow(Gtk.Window):
  15. '''
  16. Main window for Lyrebird
  17. Lyrebird is a simple and powerful voice changer for Linux, written in GTK 3.
  18. '''
  19. def __init__(self):
  20. Gtk.Window.__init__(self, title='Lyrebird')
  21. self.set_border_width(10)
  22. self.set_size_request(600, 500)
  23. self.set_default_size(600, 500)
  24. headerbar = Gtk.HeaderBar()
  25. headerbar.set_show_close_button(True)
  26. headerbar.props.title = 'Lyrebird'
  27. about_btn = Gtk.Button.new_from_icon_name('help-about-symbolic', Gtk.IconSize.BUTTON);
  28. about_btn.connect('clicked', self.about_clicked)
  29. headerbar.pack_start(about_btn)
  30. self.set_wmclass ('Lyrebird', 'Lyrebird')
  31. self.set_title('Lyrebird')
  32. self.set_titlebar(headerbar)
  33. # Set the icon
  34. self.set_icon_from_file('icon.png')
  35. # Create the lock file to ensure only one instance of Lyrebird is running at once
  36. lock_file = utils.place_lock()
  37. if lock_file is None:
  38. self.show_error_message("Lyrebird Already Running", "Only one instance of Lyrebird can be ran at a time.")
  39. exit(1)
  40. else:
  41. self.lock_file = lock_file
  42. # Setup for handling SoX process
  43. self.sox_process = None
  44. # Unload the null sink module if there is one from last time.
  45. # The only reason there would be one already, is if the application was closed without
  46. # toggling the switch to off (aka a crash was experienced).
  47. utils.unload_pa_modules()
  48. # Load the configuration file
  49. state.config = config.load_config()
  50. # Build the UI
  51. self.build_ui()
  52. def show_error_message(self, title, msg):
  53. '''
  54. Create an error message dialog with title and string message.
  55. '''
  56. dialog = Gtk.MessageDialog(
  57. parent = self,
  58. type = Gtk.MessageType.ERROR,
  59. buttons = Gtk.ButtonsType.OK,
  60. message_format = msg)
  61. dialog.set_transient_for(self)
  62. dialog.set_title(title)
  63. dialog.show()
  64. dialog.run()
  65. dialog.destroy()
  66. def build_ui(self):
  67. self.vbox = Gtk.VBox()
  68. # Toggle switch for Lyrebird
  69. self.hbox_toggle = Gtk.HBox()
  70. self.toggle_label = Gtk.Label('Toggle Lyrebird')
  71. self.toggle_label.set_halign(Gtk.Align.START)
  72. self.toggle_switch = Gtk.Switch()
  73. self.toggle_switch.set_size_request(10, 25)
  74. self.toggle_switch.connect('notify::active', self.toggle_activated)
  75. self.hbox_toggle.pack_start(self.toggle_label, False, False, 0)
  76. self.hbox_toggle.pack_end(self.toggle_switch, False, False, 0)
  77. # Pitch shift scale
  78. self.hbox_pitch = Gtk.HBox()
  79. self.pitch_label = Gtk.Label('Pitch Shift ')
  80. self.pitch_label.set_halign(Gtk.Align.START)
  81. self.pitch_adj = Gtk.Adjustment(0, -10, 10, 5, 10, 0)
  82. self.pitch_scale = Gtk.Scale(orientation=Gtk.Orientation.HORIZONTAL, adjustment=self.pitch_adj)
  83. self.pitch_scale.set_valign(Gtk.Align.CENTER)
  84. self.pitch_scale.connect('value-changed', self.pitch_scale_moved)
  85. # By default, disable the pitch shift slider to force the user to pick an effect
  86. self.pitch_scale.set_sensitive(False)
  87. self.hbox_pitch.pack_start(self.pitch_label, False, False, 0)
  88. self.hbox_pitch.pack_end(self.pitch_scale, True, True, 0)
  89. # Flow box containing the presets
  90. self.effects_label = Gtk.Label()
  91. self.effects_label.set_markup('<b>Presets</b>')
  92. self.effects_label.set_halign(Gtk.Align.START)
  93. self.flowbox = Gtk.FlowBox()
  94. self.flowbox.set_valign(Gtk.Align.START)
  95. self.flowbox.set_max_children_per_line(5)
  96. self.flowbox.set_selection_mode(Gtk.SelectionMode.NONE)
  97. # Create the flow box items
  98. self.create_flowbox_items(self.flowbox)
  99. self.vbox.pack_start(self.hbox_toggle, False, False, 5)
  100. self.vbox.pack_start(self.hbox_pitch, False, False, 5)
  101. self.vbox.pack_start(self.effects_label, False, False, 5)
  102. self.vbox.pack_end(self.flowbox, True, True, 0)
  103. self.add(self.vbox)
  104. def create_flowbox_items(self, flowbox):
  105. state.loaded_presets = presets.load_presets()
  106. for preset in state.loaded_presets:
  107. button = Gtk.Button()
  108. button.set_size_request(80, 80)
  109. button.set_label(preset.name)
  110. button.connect('clicked', self.preset_clicked)
  111. flowbox.add(button)
  112. # Event handlers
  113. def about_clicked(self, button):
  114. about = Gtk.AboutDialog()
  115. about.set_program_name('Lyrebird Voice Changer')
  116. about.set_version("v1.1.0")
  117. about.set_copyright('(c) Lyrebird 2020-2022')
  118. about.set_comments('Simple and powerful voice changer for Linux, written in GTK 3')
  119. about.set_logo(GdkPixbuf.Pixbuf.new_from_file('icon.png'))
  120. about.run()
  121. about.destroy()
  122. def toggle_activated(self, switch, gparam):
  123. if switch.get_active():
  124. # Load module-null-sink
  125. null_sink = subprocess.check_call(
  126. 'pactl load-module module-null-sink sink_name=Lyrebird-Output node.description="Lyrebird Output"'.split(' ')
  127. )
  128. remap_sink = subprocess.check_call(
  129. 'pactl load-module module-remap-source source_name=Lyrebird-Input master=Lyrebird-Output.monitor node.description="Lyrebird Virtual Input"'\
  130. .split(' ')
  131. )
  132. print(f'Loaded null output sink and remap sink')
  133. state.sink = null_sink
  134. # Kill the sox process
  135. self.terminate_sox()
  136. # Use the default preset, which is "Man" if the loaded preset is not found.
  137. default_preset = state.loaded_presets[0]
  138. current_preset = state.current_preset or default_preset
  139. if current_preset.override_pitch:
  140. # Set the pitch of the slider
  141. self.pitch_scale.set_value(float(current_preset.pitch_value))
  142. self.pitch_scale.set_sensitive(False)
  143. command = utils.build_sox_command(
  144. current_preset,
  145. config_object=state.config
  146. )
  147. else:
  148. self.pitch_scale.set_sensitive(True)
  149. command = utils.build_sox_command(
  150. current_preset,
  151. config_object=state.config,
  152. scale_object=self.pitch_scale
  153. )
  154. self.sox_process = subprocess.Popen(command.split(' '))
  155. else:
  156. utils.unload_pa_modules()
  157. self.terminate_sox()
  158. def pitch_scale_moved(self, event):
  159. global sox_multiplier
  160. # Very hacky code, we repeatedly kill sox, grab the new value to pitch shift
  161. # by, and then restart the process.
  162. # Only allow adjusting the pitch if the preset doesn't override the pitch
  163. if state.current_preset is not None:
  164. # Kill the sox process
  165. self.terminate_sox()
  166. if not state.current_preset.override_pitch:
  167. # Multiply the pitch shift scale value by the multiplier and feed it to sox
  168. command = utils.build_sox_command(
  169. state.current_preset,
  170. config_object=state.config,
  171. scale_object=self.pitch_scale
  172. )
  173. self.sox_process = subprocess.Popen(command.split(' '))
  174. def preset_clicked(self, button):
  175. global sox_multiplier
  176. self.terminate_sox()
  177. # Use a filter to find the currently selected preset
  178. current_preset = list(filter(lambda p: p.name == button.props.label, state.loaded_presets))[0]
  179. state.current_preset = current_preset
  180. if current_preset.override_pitch:
  181. # Set the pitch of the slider
  182. self.pitch_scale.set_value(float(current_preset.pitch_value))
  183. self.pitch_scale.set_sensitive(False)
  184. command = utils.build_sox_command(
  185. state.current_preset,
  186. config_object=state.config
  187. )
  188. else:
  189. self.pitch_scale.set_sensitive(True)
  190. command = utils.build_sox_command(
  191. state.current_preset,
  192. config_object=state.config,
  193. scale_object=self.pitch_scale
  194. )
  195. self.sox_process = subprocess.Popen(command.split(' '))
  196. def terminate_sox(self, timeout=1):
  197. if self.sox_process is not None:
  198. self.sox_process.terminate()
  199. try:
  200. self.sox_process.wait(timeout=timeout)
  201. except subprocess.TimeoutExpired:
  202. self.sox_process.kill()
  203. self.sox_process.wait(timeout=timeout)
  204. self.sox_process = None
  205. def close(self, *args):
  206. self.terminate_sox()
  207. self.lock_file.close()
  208. utils.destroy_lock()
  209. utils.unload_pa_modules()
  210. Gtk.main_quit()