From ac9688edd80630fdd386fdc3213aa496e58b179f Mon Sep 17 00:00:00 2001 From: SubhadeepJasu Date: Wed, 21 Jul 2021 12:58:46 +0530 Subject: [PATCH 1/4] Add media key functionality --- ....github.subhadeepjasu.ensembles.desktop.in | 7 +- src/Core/SongPlayer.vala | 104 +++++++++++++ src/Core/StylePlayer.vala | 5 + src/Core/music_player.c | 130 ++++++++++++++++ src/Core/style_player.c | 8 +- src/Interfaces/MediaKeyListener.vala | 85 ++++++++++ src/Shell/Application.vala | 47 +++++- src/Shell/MainWindow.vala | 145 +++++++++++++++++- src/Shell/Views/SongControllerView.vala | 94 +++++++++++- src/meson.build | 7 +- 10 files changed, 613 insertions(+), 19 deletions(-) create mode 100644 src/Core/SongPlayer.vala create mode 100644 src/Core/music_player.c create mode 100644 src/Interfaces/MediaKeyListener.vala diff --git a/data/com.github.subhadeepjasu.ensembles.desktop.in b/data/com.github.subhadeepjasu.ensembles.desktop.in index 8cc2509b..a544b692 100644 --- a/data/com.github.subhadeepjasu.ensembles.desktop.in +++ b/data/com.github.subhadeepjasu.ensembles.desktop.in @@ -1,11 +1,12 @@ [Desktop Entry] Name=Ensembles -GenericName=Ensembles Arranger Workstation +GenericName=Arranger Workstation Comment=Play and arrange music live as a one-person band Categories=Audio;AudioVideo;Music;Midi;Education;GTK; -Exec=com.github.subhadeepjasu.ensembles +MimeType=audio/midi +Exec=com.github.subhadeepjasu.ensembles %U Icon=com.github.subhadeepjasu.ensembles Terminal=false Type=Application Keywords=midi;virtual;music;arranger;piano;keyboard;workstation;daw; -X-GNOME-Gettext-Domain=com.github.subhadeepjasu.ensembles \ No newline at end of file +X-GNOME-Gettext-Domain=com.github.subhadeepjasu.ensembles diff --git a/src/Core/SongPlayer.vala b/src/Core/SongPlayer.vala new file mode 100644 index 00000000..c5f28c15 --- /dev/null +++ b/src/Core/SongPlayer.vala @@ -0,0 +1,104 @@ +namespace Ensembles.Core { + public class SongPlayer : Object { + public enum PlayerStatus { + READY, + PLAYING, + STOPPING, + DONE + } + public int current_file_tempo = 30; + public signal void player_status_changed (float fraction, int tempo_bpm, PlayerStatus status); + bool monitoring_player = false; + string sf_loc; + + public SongPlayer (string sf_loc, string midi_file_path) { + this.sf_loc = sf_loc; + music_player_init (sf_loc); + current_file_tempo = music_player_load_file (midi_file_path); + player_status_changed (0.0f, current_file_tempo, get_status ()); + start_monitoring (); + } + + public void start_monitoring () { + monitoring_player = true; + debug ("Starting monitor"); + new Thread ("monitor_player", monitor_player); + } + public void SongPlayer_Destroy () { + monitoring_player = false; + print ("Destruction/////////////////\n"); + music_player_destruct (); + } + + public void play () { + music_player_play (); + } + + public void pause () { + music_player_pause (); + } + + public void seek (float seek_fraction) { + music_player_seek ((int) (seek_fraction * total_ticks)); + } + + public void seek_lock (bool lock) { + if (lock) { + monitoring_player = false; + } else { + start_monitoring (); + } + } + + public void rewind () { + pause (); + seek (0.0f); + play (); + } + + public void set_repeat (bool enable) { + player_repeat = enable ? 1 : 0; + } + + public PlayerStatus get_status () { + switch (music_player_get_status ()) { + case 0: + return PlayerStatus.READY; + case 1: + return PlayerStatus.PLAYING; + case 2: + return PlayerStatus.STOPPING; + } + return PlayerStatus.DONE; + } + + private int monitor_player () { + while (monitoring_player) { + Idle.add (() => { + if (total_ticks > 0) { + player_status_changed ((float) current_ticks / (float) total_ticks, current_file_tempo, get_status ()); + } else { + player_status_changed (0.0f, current_file_tempo, get_status ()); + } + return false; + }); + Thread.yield (); + Thread.usleep (10000); + } + return 0; + } + } +} + +extern void music_player_init (string sf_loc); +extern void music_player_destruct (); +extern int music_player_load_file (string path); +extern void music_player_play (); +extern void music_player_pause (); +extern void music_player_seek (int seek_point); +extern int music_player_get_status (); + +extern int note_watch_channel; +extern int total_ticks; +extern int current_ticks; +extern int player_repeat; diff --git a/src/Core/StylePlayer.vala b/src/Core/StylePlayer.vala index 6ea7f250..9c39858d 100644 --- a/src/Core/StylePlayer.vala +++ b/src/Core/StylePlayer.vala @@ -89,6 +89,10 @@ namespace Ensembles.Core { public void change_chords (int chord_main, int chord_type) { style_player_change_chord (chord_main, chord_type); } + + public void change_tempo (int tempo) { + style_player_set_tempo (tempo); + } } } @@ -103,5 +107,6 @@ extern void style_player_queue_ending (int start, int end); extern void style_player_break (); extern void style_player_sync_start (); extern void style_player_sync_stop (); +extern void style_player_set_tempo (int tempo_bpm); extern void style_player_change_chord (int cd_main, int cd_type); diff --git a/src/Core/music_player.c b/src/Core/music_player.c new file mode 100644 index 00000000..b4c496dc --- /dev/null +++ b/src/Core/music_player.c @@ -0,0 +1,130 @@ +/*- + * Copyright (c) 2021-2022 Subhadeep Jasu + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by: Subhadeep Jasu + */ + +#include +#include +#include +#include + +// These are actually used to render audio +fluid_settings_t* mp_settings; +fluid_synth_t* mp_synth; +fluid_audio_driver_t* mp_adriver; +fluid_player_t* mp_player; + +int note_watch_channel = 0; +int total_ticks = 0; +int current_ticks = 0; + + +gchar* midi_file_path; + +int player_repeat; + +int +mp_parse_midi_events (void *data, fluid_midi_event_t *event) { + return fluid_synth_handle_midi_event (mp_synth, event); +} + +int +mp_parse_ticks (void* data, int ticks) { + current_ticks = ticks; + if (total_ticks < 10) { + total_ticks = fluid_player_get_total_ticks (mp_player); + + printf ("total_ticks = %d\n", total_ticks); + } + if (total_ticks > 10 && current_ticks + 10 > total_ticks) { + if (player_repeat > 0) { + current_ticks = 0; + return fluid_player_seek (mp_player, 1); + } else { + fluid_player_stop (mp_player); + current_ticks = 0; + return fluid_player_seek (mp_player, 1); + } + } + return FLUID_OK; +} + +void +music_player_init (const gchar* sf_loc) { + mp_settings = new_fluid_settings(); + fluid_settings_setstr(mp_settings, "audio.driver", "pulseaudio"); + fluid_settings_setint(mp_settings, "audio.periods", 16); + fluid_settings_setint(mp_settings, "audio.period-size", 4096); + fluid_settings_setint(mp_settings, "audio.realtime-prio", 40); + fluid_settings_setnum(mp_settings, "synth.gain", 1.0); + mp_synth = new_fluid_synth(mp_settings); + mp_adriver = new_fluid_audio_driver(mp_settings, mp_synth); + + if (fluid_is_soundfont(sf_loc)) { + fluid_synth_sfload(mp_synth, sf_loc, 1); + } + mp_player = new_fluid_player(mp_synth); + fluid_player_set_playback_callback(mp_player, mp_parse_midi_events, mp_synth); + fluid_player_set_tick_callback (mp_player, mp_parse_ticks, mp_synth); +} + +int +music_player_load_file (gchar* path) { + midi_file_path = (char *)malloc(sizeof (char) * strlen (path)); + strcpy (midi_file_path, path); + if (fluid_is_midifile (midi_file_path)) { + fluid_player_add (mp_player, midi_file_path); + total_ticks = fluid_player_get_total_ticks (mp_player); + return fluid_player_get_bpm (mp_player); + } + return -1; +} + +void +music_player_play () { + fluid_player_play (mp_player); +} + +void +music_player_pause () { + fluid_player_stop (mp_player); + fluid_synth_all_notes_off (mp_synth, -1); + fluid_synth_all_sounds_off (mp_synth, -1); +} + +void +music_player_seek (int seek_point) { + fluid_player_seek (mp_player, seek_point); +} + +int +music_player_get_status () { + return fluid_player_get_status (mp_player); +} + +void +music_player_destruct () { + if (mp_player) { + fluid_player_stop (mp_player); + fluid_player_join (mp_player); + delete_fluid_player(mp_player); + mp_player = NULL; + } + delete_fluid_synth(mp_synth); + delete_fluid_settings(mp_settings); + delete_fluid_audio_driver(mp_adriver); +} diff --git a/src/Core/style_player.c b/src/Core/style_player.c index 6886bd16..c7e72a1b 100644 --- a/src/Core/style_player.c +++ b/src/Core/style_player.c @@ -412,6 +412,12 @@ style_player_add_style_file (const gchar* mid_file, int reload) { } } +void +style_player_set_tempo (int tempo_bpm) { + fluid_player_set_tempo (player, FLUID_PLAYER_TEMPO_EXTERNAL_BPM, (double)tempo_bpm); + set_central_loaded_tempo (tempo_bpm); +} + void style_player_reload_style () { style_player_add_style_file (style_player_style_path, 1); @@ -420,7 +426,6 @@ style_player_reload_style () { void style_player_destruct () { /* cleanup */ - delete_fluid_audio_driver(adriver); if (player) { /* wait for playback termination */ fluid_player_stop (player); @@ -430,6 +435,7 @@ style_player_destruct () { } delete_fluid_synth(synth); delete_fluid_settings(settings); + delete_fluid_audio_driver(adriver); } void diff --git a/src/Interfaces/MediaKeyListener.vala b/src/Interfaces/MediaKeyListener.vala new file mode 100644 index 00000000..9f0a0840 --- /dev/null +++ b/src/Interfaces/MediaKeyListener.vala @@ -0,0 +1,85 @@ +/*- + * Copyright (c) 2021-2022 Subhadeep Jasu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * The Noise authors hereby grant permission for non-GPL compatible + * GStreamer plugins to be used and distributed together with GStreamer + * and Noise. This permission is above and beyond the permissions granted + * by the GPL license by which Noise is covered. If you modify this code + * you may extend this exception to your version of the code, but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. + * + * Adapted from Melody by Artem Anufrij + */ + +namespace Ensembles.Interfaces { + [DBus (name = "org.gnome.SettingsDaemon.MediaKeys")] + public interface GnomeMediaKeys : GLib.Object { + public abstract void GrabMediaPlayerKeys (string application, uint32 time) throws Error; + public abstract void ReleaseMediaPlayerKeys (string application) throws Error; + public signal void MediaPlayerKeyPressed (string application, string key); + } + + public class MediaKeyListener : GLib.Object { + public static MediaKeyListener instance { get; private set; } + public signal void media_key_pressed_play (); + public signal void media_key_pressed_pause (); + public signal void media_key_pressed_prev (); + + private GnomeMediaKeys? media_keys; + + construct { + assert (media_keys == null); + + try { + media_keys = Bus.get_proxy_sync (BusType.SESSION, "org.gnome.SettingsDaemon", "/org/gnome/SettingsDaemon/MediaKeys"); + } catch (Error e) { + warning ("Mediakeys error: %s", e.message); + } + + if (media_keys != null) { + media_keys.MediaPlayerKeyPressed.connect (pressed_key); + try { + media_keys.GrabMediaPlayerKeys (Shell.EnsemblesApp.instance.application_id, (uint32)0); + } + catch (Error err) { + warning ("Could not grab media player keys: %s", err.message); + } + } + } + + private MediaKeyListener (){} + + public static MediaKeyListener listen () { + instance = new MediaKeyListener (); + return instance; + } + + private void pressed_key (dynamic Object bus, string application, string key) { + if (application == (Shell.EnsemblesApp.instance.application_id)) { + if (key == "Previous") { + media_key_pressed_prev (); + } + else if (key == "Play") { + media_key_pressed_play (); + } + else if (key == "Pause") { + media_key_pressed_pause (); + } + } + } + } +} diff --git a/src/Shell/Application.vala b/src/Shell/Application.vala index 8837fef5..f57cb288 100644 --- a/src/Shell/Application.vala +++ b/src/Shell/Application.vala @@ -37,13 +37,16 @@ namespace Ensembles.Shell { Gtk.CssProvider css_provider; + string[] ? arg_file = null; + construct { settings = new Settings ("com.github.subhadeepjasu.ensembles"); } public EnsemblesApp () { Object ( - application_id: "com.github.subhadeepjasu.ensembles" + application_id: "com.github.subhadeepjasu.ensembles", + flags: ApplicationFlags.HANDLES_OPEN ); version_string = "1.0.0"; } @@ -51,6 +54,10 @@ namespace Ensembles.Shell { protected override void activate () { if (this.main_window == null) { this.main_window = new Ensembles.Shell.MainWindow (); + var media_key_listener = Interfaces.MediaKeyListener.listen (); + media_key_listener.media_key_pressed_play.connect (main_window.media_toggle_play); + media_key_listener.media_key_pressed_pause.connect (main_window.media_pause); + media_key_listener.media_key_pressed_prev.connect (main_window.media_prev); this.add_window (main_window); } if (css_provider == null) { @@ -66,6 +73,44 @@ namespace Ensembles.Shell { this.main_window.show_all (); } + public override void open (File[] files, string hint) { + activate (); + if (files [0].query_exists ()) { + main_window.open_file (files [0]); + } + } + + public override int command_line (ApplicationCommandLine cmd) { + command_line_interpreter (cmd); + return 0; + } + + private void command_line_interpreter (ApplicationCommandLine cmd) { + string[] args_cmd = cmd.get_arguments (); + unowned string[] args = args_cmd; + + GLib.OptionEntry [] options = new OptionEntry [2]; + options [0] = { "", 0, 0, OptionArg.STRING_ARRAY, ref arg_file, null, "URI" }; + options [1] = { null }; + + var opt_context = new OptionContext ("actions"); + opt_context.add_main_entries (options, null); + try { + opt_context.parse (ref args); + } catch (Error err) { + warning (err.message); + return; + } + + if (GLib.FileUtils.test (arg_file[0], GLib.FileTest.EXISTS) && arg_file[0].down ().has_suffix (".mid")) { + File file = File.new_for_path (arg_file[0]); + open ({ file }, ""); + return; + } + + activate (); + } + public static bool get_is_running_from_flatpak () { var flatpak_info = File.new_for_path ("/.flatpak-info"); return flatpak_info.query_exists (); diff --git a/src/Shell/MainWindow.vala b/src/Shell/MainWindow.vala index 13ad1979..7556915b 100644 --- a/src/Shell/MainWindow.vala +++ b/src/Shell/MainWindow.vala @@ -11,7 +11,7 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * - * You should have received a copy of the GNU General Public License + * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Authored by: Subhadeep Jasu @@ -38,10 +38,16 @@ namespace Ensembles.Shell { Ensembles.Core.MetronomeLFOPlayer metronome_player; Ensembles.Core.CentralBus bus; Ensembles.Core.Controller controller_connection; + Ensembles.Core.SongPlayer song_player; string sf_loc = Constants.SF2DATADIR + "/EnsemblesGM.sf2"; string sf_schema_loc = Constants.SF2DATADIR + "/EnsemblesGMSchema.csv"; string metronome_lfo_directory = Constants.PKGDATADIR + "/MetronomesAndLFO"; + + Gtk.HeaderBar headerbar; + Gtk.Scale seek_bar; + Gtk.Label custom_title_text; + Gtk.Grid custom_title_grid; public MainWindow () { Gtk.Settings settings = Gtk.Settings.get_default (); settings.gtk_application_prefer_dark_theme = true; @@ -49,14 +55,14 @@ namespace Ensembles.Shell { make_bus_events (); beat_counter_panel = new BeatCounterView (); - var headerbar = new Gtk.HeaderBar (); + headerbar = new Gtk.HeaderBar (); headerbar.has_subtitle = false; headerbar.set_show_close_button (true); headerbar.title = "Ensembles"; headerbar.pack_start (beat_counter_panel); - Gtk.Button app_menu_button = new Gtk.Button.from_icon_name ("preferences-system-symbolic", - Gtk.IconSize.BUTTON); + Gtk.Button app_menu_button = new Gtk.Button.from_icon_name ("open-menu", + Gtk.IconSize.LARGE_TOOLBAR); headerbar.pack_end (app_menu_button); this.set_titlebar (headerbar); @@ -66,9 +72,19 @@ namespace Ensembles.Shell { app_menu.popup (); }); - song_control_panel = new SongControllerView (); + song_control_panel = new SongControllerView (this); headerbar.pack_end (song_control_panel); + custom_title_text = new Gtk.Label ("Ensembles"); + custom_title_text.get_style_context ().add_class ("title"); + seek_bar = new Gtk.Scale.with_range (Gtk.Orientation.HORIZONTAL, 0, 1, 0.01); + seek_bar.set_draw_value (false); + seek_bar.width_request = 400; + custom_title_grid = new Gtk.Grid (); + custom_title_grid.attach (custom_title_text, 0, 1, 1, 1); + custom_title_grid.attach (seek_bar, 0, 2, 1, 1); + custom_title_grid.show_all (); + main_display_unit = new MainDisplayCasing (); ctrl_panel = new ControlPanel (); @@ -147,6 +163,12 @@ namespace Ensembles.Shell { bus.system_ready.connect (() => { main_display_unit.queue_remove_splash (); style_controller_view.ready (); + Timeout.add (2000, () => { + if (song_player != null) { + song_player.play (); + } + return false; + }); }); bus.style_section_change.connect ((section) => { style_controller_view.set_style_section (section); @@ -271,6 +293,47 @@ namespace Ensembles.Shell { metronome_player.beat_sync.connect (() => { beat_counter_panel.sync (); }); + song_control_panel.change_song.connect ((path) => { + queue_song (path); + song_player.play (); + }); + song_control_panel.play.connect (() => { + if (song_player != null) { + if (song_player.get_status () == Core.SongPlayer.PlayerStatus.PLAYING) { + song_player.pause (); + } else { + song_player.play (); + } + } + }); + song_control_panel.rewind.connect (() => { + if (song_player != null) { + song_player.rewind (); + } + }); + song_control_panel.change_repeat.connect ((active) => { + if (song_player != null) { + song_player.set_repeat (active); + } + }); + seek_bar.change_value.connect (() => { + if (song_player != null) { + song_player.seek ((float) (seek_bar.get_value ())); + } + return false; + }); + seek_bar.button_press_event.connect (() => { + if (song_player != null) { + song_player.seek_lock (true); + } + return false; + }); + seek_bar.button_release_event.connect (() => { + if (song_player != null) { + song_player.seek_lock (false); + } + return false; + }); this.destroy.connect (() => { slider_board.stop_monitoring (); @@ -294,5 +357,77 @@ namespace Ensembles.Shell { detected_voice_indices = voice_analyser.get_all_category_indices (); main_display_unit.update_voice_list (detected_voices); } + + private void update_header_bar (float fraction, int tempo_bpm, Core.SongPlayer.PlayerStatus status) { + if (status == Core.SongPlayer.PlayerStatus.PLAYING || status == Core.SongPlayer.PlayerStatus.READY) { + if (headerbar.get_custom_title () == null) { + headerbar.set_custom_title (custom_title_grid); + } + seek_bar.set_value ((double) fraction); + } else { + headerbar.set_custom_title (null); + seek_bar.set_value (0); + } + if (status == Core.SongPlayer.PlayerStatus.PLAYING) { + song_control_panel.set_playing (true); + } else { + song_control_panel.set_playing (false); + } + } + + public void open_file (File file) { + string path = file.get_path (); + queue_song (path); + } + + public void queue_song (string path) { + if (song_player != null) { + song_player.player_status_changed.disconnect (update_header_bar); + song_player.SongPlayer_Destroy (); + song_player = null; + } + song_player = new Core.SongPlayer (sf_loc, path); + int song_tempo = song_player.current_file_tempo; + song_player.player_status_changed.connect (update_header_bar); + print ("Tempp %d\n", song_tempo); + style_player.change_tempo (song_tempo); + main_display_unit.set_tempo_display (song_tempo); + try { + Regex regex = new Regex ("[ \\w-]+?(?=\\.)"); + MatchInfo match_info; + if (regex.match (path, 0, out match_info)) { + custom_title_text.set_text ("Now Playing - " + match_info.fetch (0)); + } + } catch (RegexError e) { + warning (e.message); + } + song_control_panel.set_player_active (); + } + + public void media_toggle_play () { + if (song_player != null) { + if (song_player.get_status () == Core.SongPlayer.PlayerStatus.PLAYING) { + song_player.pause (); + } else { + song_player.play (); + } + } else { + style_player.play_style (); + } + } + + public void media_pause () { + if (song_player != null) { + song_player.pause (); + } else { + style_player.sync_stop (); + } + } + + public void media_prev () { + if (song_player != null) { + song_player.rewind (); + } + } } } diff --git a/src/Shell/Views/SongControllerView.vala b/src/Shell/Views/SongControllerView.vala index 2dabd2e1..b0417d95 100644 --- a/src/Shell/Views/SongControllerView.vala +++ b/src/Shell/Views/SongControllerView.vala @@ -19,22 +19,102 @@ namespace Ensembles.Shell { public class SongControllerView : Gtk.Grid { - Gtk.Button prev_button; + Gtk.Button rewind_button; Gtk.Button play_button; - Gtk.Button next_button; + Gtk.Button repeat_button; + Gtk.Button open_file_button; - public SongControllerView () { - prev_button = new Gtk.Button.from_icon_name ("media-skip-backward-symbolic", Gtk.IconSize.LARGE_TOOLBAR); + Gtk.FileChooserDialog file_chooser; + + Gtk.Window mainwindow; + + bool repeat_on; + + public signal void change_song (string path); + public signal void play (); + public signal void change_repeat (bool active); + public signal void rewind (); + + public SongControllerView (Gtk.Window mainwindow) { + this.mainwindow = mainwindow; + rewind_button = new Gtk.Button.from_icon_name ("media-seek-backward-symbolic", Gtk.IconSize.LARGE_TOOLBAR); play_button = new Gtk.Button.from_icon_name ("media-playback-start-symbolic", Gtk.IconSize.LARGE_TOOLBAR); - next_button = new Gtk.Button.from_icon_name ("media-skip-forward-symbolic", Gtk.IconSize.LARGE_TOOLBAR); + repeat_button = new Gtk.Button.from_icon_name ("media-playlist-no-repeat-symbolic", Gtk.IconSize.LARGE_TOOLBAR); + open_file_button = new Gtk.Button.from_icon_name ("document-open-symbolic", Gtk.IconSize.LARGE_TOOLBAR); - attach (prev_button, 0, 0, 1, 1); + play_button.sensitive = false; + rewind_button.sensitive = false; + repeat_button.sensitive = false; + + attach (rewind_button, 0, 0, 1, 1); attach (play_button, 1, 0, 1, 1); - attach (next_button, 2, 0, 1, 1); + attach (repeat_button, 2, 0, 1, 1); + attach (open_file_button, 3, 0, 1, 1); margin_end = 8; this.show_all (); + + file_chooser = new Gtk.FileChooserDialog (_("Open MIDI Song"), + mainwindow, + Gtk.FileChooserAction.OPEN, + _("Cancel"), + Gtk.ResponseType.CANCEL, + _("Open"), + Gtk.ResponseType.ACCEPT + ); + file_chooser.local_only = false; + file_chooser.modal = true; + + var file_filter_midi = new Gtk.FileFilter (); + file_filter_midi.add_mime_type ("audio/midi"); + file_filter_midi.set_filter_name ("MIDI Sequence"); + file_chooser.add_filter (file_filter_midi); + + open_file_button.clicked.connect (() => { + file_chooser.run (); + file_chooser.hide (); + }); + + file_chooser.response.connect ((response_id) => { + if (response_id == -3) { + string current_file_path = file_chooser.get_file ().get_path (); + change_song (current_file_path); + } + }); + + repeat_button.clicked.connect (() => { + if (repeat_on) { + repeat_on = false; + repeat_button.set_image (new Gtk.Image.from_icon_name ("media-playlist-no-repeat-symbolic", Gtk.IconSize.LARGE_TOOLBAR)); + } else { + repeat_on = true; + repeat_button.set_image (new Gtk.Image.from_icon_name ("media-playlist-repeat-one-symbolic", Gtk.IconSize.LARGE_TOOLBAR)); + } + change_repeat (repeat_on); + }); + + play_button.clicked.connect (() => { + play (); + }); + + rewind_button.clicked.connect (() => { + rewind (); + }); + } + + public void set_playing (bool playing) { + if (playing) { + play_button.set_image (new Gtk.Image.from_icon_name ("media-playback-pause-symbolic", Gtk.IconSize.LARGE_TOOLBAR)); + } else { + play_button.set_image (new Gtk.Image.from_icon_name ("media-playback-start-symbolic", Gtk.IconSize.LARGE_TOOLBAR)); + } + } + + public void set_player_active () { + play_button.sensitive = true; + rewind_button.sensitive = true; + repeat_button.sensitive = true; } } } diff --git a/src/meson.build b/src/meson.build index d404d975..a8afc539 100644 --- a/src/meson.build +++ b/src/meson.build @@ -53,7 +53,9 @@ ensembles_sources_vala = files ( 'Core/Voice.vala', 'Core/VoiceAnalyser.vala', 'Core/SamplePlayer.vala', - 'Core/SampleRecorder.vala' + 'Core/SampleRecorder.vala', + 'Core/SongPlayer.vala', + 'Interfaces/MediaKeyListener.vala' ) ensembles_sources_c = files ( @@ -65,7 +67,8 @@ ensembles_sources_c = files ( 'Core/metronome_lfo_player.c', 'Core/style_analyser.c', 'Core/chord_finder.c', - 'Core/voice_analyser.c' + 'Core/voice_analyser.c', + 'Core/music_player.c' ) ensembles_sources = [ From 2b0cfad0c9a0d7626c565a840cdb700a197aa2b6 Mon Sep 17 00:00:00 2001 From: SubhadeepJasu Date: Wed, 21 Jul 2021 14:55:07 +0530 Subject: [PATCH 2/4] Enable sound indicator for song player --- src/Core/music_player.c | 5 +- src/Interfaces/SoundIndicator.vala | 179 +++++++++++++++++++++++++++++ src/Shell/Application.vala | 4 +- src/Shell/MainWindow.vala | 17 ++- src/meson.build | 3 +- 5 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 src/Interfaces/SoundIndicator.vala diff --git a/src/Core/music_player.c b/src/Core/music_player.c index b4c496dc..d9d7e407 100644 --- a/src/Core/music_player.c +++ b/src/Core/music_player.c @@ -113,7 +113,10 @@ music_player_seek (int seek_point) { int music_player_get_status () { - return fluid_player_get_status (mp_player); + if (mp_player) { + return fluid_player_get_status (mp_player); + } + return 0; } void diff --git a/src/Interfaces/SoundIndicator.vala b/src/Interfaces/SoundIndicator.vala new file mode 100644 index 00000000..c11ab5e2 --- /dev/null +++ b/src/Interfaces/SoundIndicator.vala @@ -0,0 +1,179 @@ +/*- + * Copyright (c) 2021-2022 Subhadeep Jasu + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * The Noise authors hereby grant permission for non-GPL compatible + * GStreamer plugins to be used and distributed together with GStreamer + * and Noise. This permission is above and beyond the permissions granted + * by the GPL license by which Noise is covered. If you modify this code + * you may extend this exception to your version of the code, but you are not + * obligated to do so. If you do not wish to do so, delete this exception + * statement from your version. + * + * Adapted from Melody by Artem Anufrij + */ + + namespace Ensembles.Interfaces { + public class SoundIndicator { + public static SoundIndicator instance { get; private set; } + public static SoundIndicator listen (Shell.MainWindow main_window) { + instance = new SoundIndicator (); + instance.initialize (main_window); + return instance; + } + + SoundIndicatorPlayer player; + SoundIndicatorRoot root; + + unowned DBusConnection conn; + uint owner_id; + uint root_id; + uint player_id; + + Shell.MainWindow main_window; + + private void initialize (Shell.MainWindow main_window) { + owner_id = Bus.own_name (BusType.SESSION, "org.mpris.MediaPlayer2.Ensembles", GLib.BusNameOwnerFlags.NONE, on_bus_acquired, on_name_acquired, on_name_lost); + if (owner_id == 0) { + warning ("Could not initialize MPRIS session.\n"); + } + this.main_window = main_window; + main_window.destroy.connect (() => { + this.conn.unregister_object (root_id); + this.conn.unregister_object (player_id); + Bus.unown_name (owner_id); + }); + } + + private void on_bus_acquired (DBusConnection connection, string name) { + this.conn = connection; + try { + root = new SoundIndicatorRoot (); + root_id = connection.register_object ("/org/mpris/MediaPlayer2", root); + player = new SoundIndicatorPlayer (connection, main_window); + player_id = connection.register_object ("/org/mpris/MediaPlayer2", player); + } + catch(Error e) { + warning ("could not create MPRIS player: %s\n", e.message); + } + } + + private void on_name_acquired (DBusConnection connection, string name) { + } + + private void on_name_lost (DBusConnection connection, string name) { + } + + public void change_song_state (string song_name, Core.SongPlayer.PlayerStatus status) { + player.player_state_changed (song_name, status); + } + } + + [DBus(name = "org.mpris.MediaPlayer2")] + public class SoundIndicatorRoot : GLib.Object { + Shell.EnsemblesApp app; + + construct { + this.app = Shell.EnsemblesApp.instance; + } + + public string DesktopEntry { + owned get { + return app.application_id; + } + } + } + + [DBus(name = "org.mpris.MediaPlayer2.Player")] + public class SoundIndicatorPlayer : GLib.Object { + DBusConnection connection; + Shell.EnsemblesApp app; + Shell.MainWindow main_window; + + public SoundIndicatorPlayer (DBusConnection connection, Shell.MainWindow main_window) { + this.app = Shell.EnsemblesApp.instance; + this.main_window = main_window; + this.connection = connection; + } + + private static string[] get_simple_string_array (string text) { + string[] array = new string[0]; + array += text; + return array; + } + + private void send_properties (string property, Variant val) { + var property_list = new HashTable (str_hash, str_equal); + property_list.insert (property, val); + + var builder = new VariantBuilder (VariantType.ARRAY); + var invalidated_builder = new VariantBuilder (new VariantType("as")); + + foreach(string name in property_list.get_keys ()) { + Variant variant = property_list.lookup (name); + builder.add ("{sv}", name, variant); + } + + try { + connection.emit_signal (null, + "/org/mpris/MediaPlayer2", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + new Variant("(sa{sv}as)", "org.mpris.MediaPlayer2.Player", builder, invalidated_builder)); + } + catch(Error e) { + print("Could not send MPRIS property change: %s\n", e.message); + } + } + + public bool CanGoNext { get { return false; } } + + public bool CanGoPrevious { get { return true; } } + + public bool CanPlay { get { return true; } } + + public bool CanPause { get { return true; } } + + public void PlayPause () throws Error { + main_window.media_toggle_play (); + } + + public void Next () throws Error { + // Do nothing + } + + public void Previous() throws Error { + main_window.media_prev (); + } + + public void player_state_changed (string song_name, Core.SongPlayer.PlayerStatus status) { + Variant property; + if (status == Core.SongPlayer.PlayerStatus.PLAYING) { + property = "Playing"; + var metadata = new HashTable (null, null); + metadata.insert("xesam:title", "Ensembles Song Player"); + metadata.insert("xesam:artist", get_simple_string_array(song_name)); + send_properties ("Metadata", metadata); + } else { + property = "Stopped"; + var metadata = new HashTable (null, null); + metadata.insert("xesam:title", "Ensembles Song Player"); + metadata.insert("xesam:artist", get_simple_string_array("Not Playing")); + send_properties ("Metadata", metadata); + } + send_properties ("PlaybackStatus", property); + } + } +} diff --git a/src/Shell/Application.vala b/src/Shell/Application.vala index f57cb288..d5fe5cab 100644 --- a/src/Shell/Application.vala +++ b/src/Shell/Application.vala @@ -33,7 +33,7 @@ namespace Ensembles.Shell { public static Settings settings; - Ensembles.Shell.MainWindow main_window; + public Ensembles.Shell.MainWindow main_window; Gtk.CssProvider css_provider; @@ -59,6 +59,8 @@ namespace Ensembles.Shell { media_key_listener.media_key_pressed_pause.connect (main_window.media_pause); media_key_listener.media_key_pressed_prev.connect (main_window.media_prev); this.add_window (main_window); + var sound_indicator_listener = Interfaces.SoundIndicator.listen (main_window); + main_window.song_player_state_changed.connect_after (sound_indicator_listener.change_song_state); } if (css_provider == null) { css_provider = new Gtk.CssProvider (); diff --git a/src/Shell/MainWindow.vala b/src/Shell/MainWindow.vala index 7556915b..9d72056b 100644 --- a/src/Shell/MainWindow.vala +++ b/src/Shell/MainWindow.vala @@ -48,6 +48,10 @@ namespace Ensembles.Shell { Gtk.Scale seek_bar; Gtk.Label custom_title_text; Gtk.Grid custom_title_grid; + + string song_name; + + public signal void song_player_state_changed (string song_name, Core.SongPlayer.PlayerStatus status); public MainWindow () { Gtk.Settings settings = Gtk.Settings.get_default (); settings.gtk_application_prefer_dark_theme = true; @@ -301,8 +305,10 @@ namespace Ensembles.Shell { if (song_player != null) { if (song_player.get_status () == Core.SongPlayer.PlayerStatus.PLAYING) { song_player.pause (); + song_player_state_changed (song_name, Core.SongPlayer.PlayerStatus.READY); } else { song_player.play (); + song_player_state_changed (song_name, Core.SongPlayer.PlayerStatus.PLAYING); } } }); @@ -343,6 +349,11 @@ namespace Ensembles.Shell { metronome_player.unref (); debug ("CLEANUP: Unloading Style Engine\n"); style_player.unref (); + if (song_player != null) { + debug ("CLEANUP: Unloading Song Player\n"); + song_player.SongPlayer_Destroy (); + song_player = null; + } debug ("CLEANUP: Unloading Synthesizer\n"); synthesizer.unref (); debug ("CLEANUP: Unloading Central Bus\n"); @@ -396,8 +407,10 @@ namespace Ensembles.Shell { Regex regex = new Regex ("[ \\w-]+?(?=\\.)"); MatchInfo match_info; if (regex.match (path, 0, out match_info)) { - custom_title_text.set_text ("Now Playing - " + match_info.fetch (0)); + song_name = match_info.fetch (0); + custom_title_text.set_text ("Now Playing - " + song_name); } + song_player_state_changed (song_name, Core.SongPlayer.PlayerStatus.READY); } catch (RegexError e) { warning (e.message); } @@ -408,8 +421,10 @@ namespace Ensembles.Shell { if (song_player != null) { if (song_player.get_status () == Core.SongPlayer.PlayerStatus.PLAYING) { song_player.pause (); + song_player_state_changed (song_name, Core.SongPlayer.PlayerStatus.READY); } else { song_player.play (); + song_player_state_changed (song_name, Core.SongPlayer.PlayerStatus.PLAYING); } } else { style_player.play_style (); diff --git a/src/meson.build b/src/meson.build index a8afc539..fe888c05 100644 --- a/src/meson.build +++ b/src/meson.build @@ -55,7 +55,8 @@ ensembles_sources_vala = files ( 'Core/SamplePlayer.vala', 'Core/SampleRecorder.vala', 'Core/SongPlayer.vala', - 'Interfaces/MediaKeyListener.vala' + 'Interfaces/MediaKeyListener.vala', + 'Interfaces/SoundIndicator.vala' ) ensembles_sources_c = files ( From 7b6182842db65873f200228452a5de98d6bd1b1d Mon Sep 17 00:00:00 2001 From: SubhadeepJasu Date: Wed, 21 Jul 2021 17:45:40 +0530 Subject: [PATCH 3/4] Fix MPRIS ownership for flatpak --- com.github.subhadeepjasu.ensembles.yml | 3 +++ ....github.subhadeepjasu.ensembles.desktop.in | 2 +- src/Core/SongPlayer.vala | 5 ++-- src/Interfaces/SoundIndicator.vala | 7 +++++- src/Shell/MainWindow.vala | 24 ++++++++++++------- 5 files changed, 27 insertions(+), 14 deletions(-) diff --git a/com.github.subhadeepjasu.ensembles.yml b/com.github.subhadeepjasu.ensembles.yml index 78422626..90ebd680 100644 --- a/com.github.subhadeepjasu.ensembles.yml +++ b/com.github.subhadeepjasu.ensembles.yml @@ -10,6 +10,9 @@ finish-args: - '--socket=pulseaudio' - '--device=all' - '--filesystem=home' + - '--system-talk-name=org.freedesktop.Accounts' + - '--own-name=org.mpris.MediaPlayer2.com.github.subhadeepjasu.ensembles' + - '--talk-name=org.gnome.SettingsDaemon.MediaKeys' modules: - name: granite buildsystem: meson diff --git a/data/com.github.subhadeepjasu.ensembles.desktop.in b/data/com.github.subhadeepjasu.ensembles.desktop.in index a544b692..c259a0fa 100644 --- a/data/com.github.subhadeepjasu.ensembles.desktop.in +++ b/data/com.github.subhadeepjasu.ensembles.desktop.in @@ -8,5 +8,5 @@ Exec=com.github.subhadeepjasu.ensembles %U Icon=com.github.subhadeepjasu.ensembles Terminal=false Type=Application -Keywords=midi;virtual;music;arranger;piano;keyboard;workstation;daw; +Keywords=midi;virtual;music;arranger;piano;keyboard;workstation; X-GNOME-Gettext-Domain=com.github.subhadeepjasu.ensembles diff --git a/src/Core/SongPlayer.vala b/src/Core/SongPlayer.vala index c5f28c15..4362ad9e 100644 --- a/src/Core/SongPlayer.vala +++ b/src/Core/SongPlayer.vala @@ -24,9 +24,8 @@ namespace Ensembles.Core { debug ("Starting monitor"); new Thread ("monitor_player", monitor_player); } - public void SongPlayer_Destroy () { + public void songplayer_destroy () { monitoring_player = false; - print ("Destruction/////////////////\n"); music_player_destruct (); } @@ -75,7 +74,7 @@ namespace Ensembles.Core { private int monitor_player () { while (monitoring_player) { Idle.add (() => { - if (total_ticks > 0) { + if (total_ticks > 0 && monitoring_player) { player_status_changed ((float) current_ticks / (float) total_ticks, current_file_tempo, get_status ()); } else { player_status_changed (0.0f, current_file_tempo, get_status ()); diff --git a/src/Interfaces/SoundIndicator.vala b/src/Interfaces/SoundIndicator.vala index c11ab5e2..4685006c 100644 --- a/src/Interfaces/SoundIndicator.vala +++ b/src/Interfaces/SoundIndicator.vala @@ -45,7 +45,12 @@ Shell.MainWindow main_window; private void initialize (Shell.MainWindow main_window) { - owner_id = Bus.own_name (BusType.SESSION, "org.mpris.MediaPlayer2.Ensembles", GLib.BusNameOwnerFlags.NONE, on_bus_acquired, on_name_acquired, on_name_lost); + owner_id = Bus.own_name (BusType.SESSION, + "org.mpris.MediaPlayer2.com.github.subhadeepjasu.ensembles", + GLib.BusNameOwnerFlags.NONE, + on_bus_acquired, + null, + null); if (owner_id == 0) { warning ("Could not initialize MPRIS session.\n"); } diff --git a/src/Shell/MainWindow.vala b/src/Shell/MainWindow.vala index 9d72056b..f7c219d9 100644 --- a/src/Shell/MainWindow.vala +++ b/src/Shell/MainWindow.vala @@ -55,6 +55,7 @@ namespace Ensembles.Shell { public MainWindow () { Gtk.Settings settings = Gtk.Settings.get_default (); settings.gtk_application_prefer_dark_theme = true; + debug ("STARTUP: Loading Central Bus"); bus = new Ensembles.Core.CentralBus (); make_bus_events (); @@ -123,6 +124,7 @@ namespace Ensembles.Shell { this.add (grid); this.show_all (); + debug ("STARTUP: Loading MIDI Input Monitor"); controller_connection = new Ensembles.Core.Controller (); app_menu.change_enable_midi_input.connect ((enable) => { if (enable) { @@ -130,10 +132,13 @@ namespace Ensembles.Shell { app_menu.update_devices (devices_found); } }); + debug ("STARTUP: Loading Synthesizer"); synthesizer = new Ensembles.Core.Synthesizer (sf_loc); main_keyboard.connect_synthesizer (synthesizer); + debug ("STARTUP: Loading Style Engine"); style_player = new Ensembles.Core.StylePlayer (); + debug ("STARTUP: Discovering Styles"); style_discovery = new Ensembles.Core.StyleDiscovery (); style_discovery.analysis_complete.connect (() => { style_player.add_style_file (style_discovery.style_files.nth_data (0)); @@ -145,6 +150,7 @@ namespace Ensembles.Shell { ); }); + debug ("STARTUP: Loading Metronome and LFO Engine"); metronome_player = new Ensembles.Core.MetronomeLFOPlayer (metronome_lfo_directory); make_ui_events (); @@ -343,20 +349,20 @@ namespace Ensembles.Shell { this.destroy.connect (() => { slider_board.stop_monitoring (); - debug ("CLEANUP: Unloading MIDI Controller Monitor\n"); + debug ("CLEANUP: Unloading MIDI Input Monitor"); controller_connection.unref (); - debug ("CLEANUP: Unloading Metronome and LFO Engine\n"); + debug ("CLEANUP: Unloading Metronome and LFO Engine"); metronome_player.unref (); - debug ("CLEANUP: Unloading Style Engine\n"); + debug ("CLEANUP: Unloading Style Engine"); style_player.unref (); if (song_player != null) { - debug ("CLEANUP: Unloading Song Player\n"); - song_player.SongPlayer_Destroy (); + debug ("CLEANUP: Unloading Song Player"); + song_player.songplayer_destroy (); song_player = null; } - debug ("CLEANUP: Unloading Synthesizer\n"); + debug ("CLEANUP: Unloading Synthesizer"); synthesizer.unref (); - debug ("CLEANUP: Unloading Central Bus\n"); + debug ("CLEANUP: Unloading Central Bus"); bus.unref (); }); debug ("Initialized\n"); @@ -394,13 +400,13 @@ namespace Ensembles.Shell { public void queue_song (string path) { if (song_player != null) { song_player.player_status_changed.disconnect (update_header_bar); - song_player.SongPlayer_Destroy (); + song_player.songplayer_destroy (); song_player = null; } + debug ("Creating new Song Player instance with midi file: %s", path); song_player = new Core.SongPlayer (sf_loc, path); int song_tempo = song_player.current_file_tempo; song_player.player_status_changed.connect (update_header_bar); - print ("Tempp %d\n", song_tempo); style_player.change_tempo (song_tempo); main_display_unit.set_tempo_display (song_tempo); try { From 04e4082119ce0b32eefc529cf8443c43d6c29b11 Mon Sep 17 00:00:00 2001 From: SubhadeepJasu Date: Thu, 22 Jul 2021 12:23:07 +0530 Subject: [PATCH 4/4] Skip Lint for Interface files --- com.github.subhadeepjasu.ensembles.yml | 2 ++ src/Interfaces/MediaKeyListener.vala | 4 +++- src/Interfaces/SoundIndicator.vala | 14 +++++--------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/com.github.subhadeepjasu.ensembles.yml b/com.github.subhadeepjasu.ensembles.yml index 90ebd680..0ec1ed8c 100644 --- a/com.github.subhadeepjasu.ensembles.yml +++ b/com.github.subhadeepjasu.ensembles.yml @@ -10,7 +10,9 @@ finish-args: - '--socket=pulseaudio' - '--device=all' - '--filesystem=home' + # Required for system wide dark style preference - '--system-talk-name=org.freedesktop.Accounts' + # Required for media keys and MPRIS access - '--own-name=org.mpris.MediaPlayer2.com.github.subhadeepjasu.ensembles' - '--talk-name=org.gnome.SettingsDaemon.MediaKeys' modules: diff --git a/src/Interfaces/MediaKeyListener.vala b/src/Interfaces/MediaKeyListener.vala index 9f0a0840..ac0bfe30 100644 --- a/src/Interfaces/MediaKeyListener.vala +++ b/src/Interfaces/MediaKeyListener.vala @@ -25,6 +25,8 @@ * Adapted from Melody by Artem Anufrij */ +// vala-lint=skip-file + namespace Ensembles.Interfaces { [DBus (name = "org.gnome.SettingsDaemon.MediaKeys")] public interface GnomeMediaKeys : GLib.Object { @@ -61,7 +63,7 @@ namespace Ensembles.Interfaces { } } - private MediaKeyListener (){} + private MediaKeyListener () {} public static MediaKeyListener listen () { instance = new MediaKeyListener (); diff --git a/src/Interfaces/SoundIndicator.vala b/src/Interfaces/SoundIndicator.vala index 4685006c..1889c97f 100644 --- a/src/Interfaces/SoundIndicator.vala +++ b/src/Interfaces/SoundIndicator.vala @@ -25,6 +25,8 @@ * Adapted from Melody by Artem Anufrij */ +// vala-lint=skip-file + namespace Ensembles.Interfaces { public class SoundIndicator { public static SoundIndicator instance { get; private set; } @@ -47,8 +49,8 @@ private void initialize (Shell.MainWindow main_window) { owner_id = Bus.own_name (BusType.SESSION, "org.mpris.MediaPlayer2.com.github.subhadeepjasu.ensembles", - GLib.BusNameOwnerFlags.NONE, - on_bus_acquired, + GLib.BusNameOwnerFlags.NONE, + on_bus_acquired, null, null); if (owner_id == 0) { @@ -70,17 +72,11 @@ player = new SoundIndicatorPlayer (connection, main_window); player_id = connection.register_object ("/org/mpris/MediaPlayer2", player); } - catch(Error e) { + catch (Error e) { warning ("could not create MPRIS player: %s\n", e.message); } } - private void on_name_acquired (DBusConnection connection, string name) { - } - - private void on_name_lost (DBusConnection connection, string name) { - } - public void change_song_state (string song_name, Core.SongPlayer.PlayerStatus status) { player.player_state_changed (song_name, status); }