Простой аудиоплеер на BASS и GTK 2.0


Ранее я публиковал статью о консольном плеере, входящем в состав примеров для библиотеки BASS. Хотелось бы воплотить нечто подобное в виде графического приложения, но с возможностью перетаскивать треки на окно плеера и, в ходе воспроизведения, видеть информацию из мета тегов.
Для своего проекта я буду использовать GTK 2.0. Весь исходный код фактически является модифицированной версией кода из tutorial к GTK 2.0. Кроме того, в справке к BASS имеются готовые примеры кода. В частности пример чтения серии строк с нулевым байтом в конце.
Цели
Вобщем, напишем свой плеер, на окно которого можно перетаскивать файлы из менеджера (ссылки из браузера, соответствующие радиостанциям, строки — пути к файлам и т.д.), добавляя таким образом их в плейлист.
Интерфейс
Далее поговорим об интерфейсе. Плейлист (раскрывающийся список GtkComboBoxText), кнопки Play и Stop (GtkButton), индикатор позиции воспроизведения (GtkHScale), показатель длительности воспроизведения (GtkLabel), уровни левого и правого каналов (GtkProgressBar) — пока всё, что нам нужно.
Конечно можно было добавить индикатор уровня громкости, более гибкую систему работы с плейлистом, но эти улучшения могут быть рассмотрены после решения основных задачь.
Каркас
Для успешной компиляции начатьного проекта, я рекомендую создать новую папку в директории с примерами для BASS и скопировать тура makefile из какого-нибуть другого примера.
Makefile
TARGET = simple
PACKAGES = gtk+-2.0
include ../makefile.in
all: $(TARGET)
clean:
$(RM) $(TARGET)
Как видно из содержимого, наш проект будет называться simple. Для успешной компиляции, основной исходник назовем simple.c.
В данной статье я не буду описывать процесс решения тривиальных задач с GTK 2.0, остановлюсь только на ключевых моментах, над которыми пришлось поразмыслить.
Инициализация BASS
Настройка и выбор воспроизводящего устройства происходит в методе MyInitBASS(). Устройство выбрано по умолчанию — первое физическое, частота дискретизации — 44100. В конфигурации выбрана возможность проигрывать плейлисты из интернета.
Формирование интерфейса в GTK 2.0
Во всех примерах для BASS используется glade, но я решил сформировать интерфейс программно. Такое решение было принято из соображений избавить себя от хлопот с версиями этого самого glade и соответствующих редакторов форм. Все виджеты определены глобально следующим образом.
GtkWidget* progressL;
GtkWidget* progressR;
GtkWidget* songList;
GtkWidget* playButton;
GtkWidget* stopButton;
GtkWidget* slidePlay;
GtkWidget* timeBar;
Для размещения виджетов на основном окне использовались горизонтальные и вертикальные контейнеры.
GtkWidget* box1 = gtk_hbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(box0), box1, TRUE, TRUE, 0);
--------------progressL = gtk_progress_bar_new(); gtk_progress_bar_set_orientation((GtkProgressBar*)(progressL), GTK_PROGRESS_BOTTOM_TO_TOP);
gtk_box_pack_start(GTK_BOX(box1), progressL, FALSE, TRUE, 0);
Фрагмент выше определяет горизонтальный контейнер, в который, в свою очередь, добавляется виджет уровня левого канала.
Особенности индикатора позиции воспроизведения
Есть один интересный момент. Текущее значение горизонтального «бегунка» отображается по умолчанию в виде целого числа, что несовсем приемлемо в нашем случае. Есть ли способ заменить это число на текущее время? Да, есть. Для подобной ситуации существует специальный сигнал format-value для виджета. Следовательно, реализация будет выглядеть примерно так.
slidePlay = gtk_hscale_new_with_range(0, 1000, 1); gtk_signal_connect(slidePlay, "change-value", G_CALLBACK(slidePlayChanged), NULL);
gtk_signal_connect(slidePlay, "format-value", G_CALLBACK(timeFormat), NULL);
Тут следует пояснить, что 1000 — это максимальное значение слайдера. Тоесть фактически мы, перемещая слайд, будем выбирать не конкретную позицию воспроизведения, а относительную. Можно заменить это значение на большее при желании, тогда точность выбора позиции возрастет. А вот и сама функция для формата значения.
static gchar* timeFormat(GtkScale* s, gdouble value, gpointer user_data) {
if(main_chan) {
QWORD pos = BASS_ChannelGetPosition(main_chan, BASS_POS_BYTE);
DWORD time = BASS_ChannelBytes2Seconds(main_chan, pos);
return g_strdup_printf("%02u:%02u", time / 60, time % 60);
};
return g_strdup_printf ("-->%0.*g<--", gtk_scale_get_digits(s), value);
};
Здесь всё просто: если канал воспроизведения активен, получаем текущую позицию, переводим её из байт в секунды, рассчитываем количество минут и секунд, возвращаем сформированную строку.
Плейлист
Обычно рекомендуется отделять алгоритмы связанные с интерфейсом и те, что составляют суть вашей рограммы. Данный прием позволит удобно и эффективно откорректировать приложение в случае изменения способа подачи информации. Передумали использовать GTK и решили переделать программу под Windows? Всё, что для этого нужно — поместить такие методы как getPlaylistNode(int n) в нужные места и частично изменить часть других, а не формировать всё с нуля. Определим плейлист как двунаправленный список.
typedef struct {
char* info;
char* fname;
struct MyPlayList* next;
struct MyPlayList* prev;
} MyPlayList;
Здесь info — текст для отображения в списке, fname — путь к аудиофайлу на локальном носителе или URL.
GTK 2.0 Drag and Drop
Вот здесь было немало проблем, так как механизм довольно не прост. В первую очередь определим — какого рода данные можно будет перетаскивать в наш плейлист.
static GtkTargetEntry target_list[] = {
{ "text/plain", 0, TARGET_STRING },
{ "text/uri-list", 0, TARGET_URL }
};
static guint n_targets = G_N_ELEMENTS (target_list);
Будем обрабатывать либо список ссылок (text/uri-list), либо фрагмент неформатированного текста (text/plain). Первый MIME тип нам понадобится для перетаскивания списка файлов из менеджера, а второй для переноса ссылки из браузера или какого-либо другого места.
Следующим шагом будет указание виджета, принимающего данные. Это будет songList.
gtk_drag_dest_set(songList,
GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_HIGHLIGHT,
target_list, n_targets, GDK_ACTION_COPY);
Далее привяжем сигналы drag-data-received, drag-leave, drag-motion и drag-drop к виджету songList и определим соответствующие методы. Содержание каждого метода не сильно отличается от примера из руководства, но существует критичный момент, связаный с выбором принимаемых данных. В примере написано следующее.
/* Choose the best target type */
target_type = GDK_POINTER_TO_ATOM(
g_list_nth_data (gdk_drag_context_list_targets(context),
TARGET_INT32));
/* Request the data from the source. */
gtk_drag_get_data(widget, context, target_type, time);
Так как замена TARGET_INT32 на TARGET_STRING или на TARGET_URL не дала результатов (сигнал drag-data-received просто не срабатывал), было найдено следующее решение с использованием функции gdk_atom_intern.
target_type = gdk_atom_intern("text/uri-list", FALSE);
gtk_drag_get_data(widget, context, target_type, time);
Аналогичную пару строк кода добавляем для типа text/plain. Если перетащили файл (файлы), то срабатывает случай с text/uri-list, а второй оказывается неудачным. Если перетащили текст, то наоборот.
Замена текста строки в GtkComboBoxText
Я, знакомясь с GTK 2.0, ожидал найти какой-нибудь простой метод для решения подобной задачи. Что-то вроде gtk_combo_box_text_set_item_text. Но среди доступных методов есть только те, что вставляют новую строку в список разными способами или удаляют её. Лично мне в данном случае не удалось обнаружить вариант замены содержимого строки без вызова сигнала выбора из списка. Дело в том, что на этот сигнал завязана процедура воспроизведения трека. А если трек начинает воспроизводиться, то мы получаем метаданные о нем и записываем их в название. Таким образом круг замкнулся. Так как бесконечный цикл — это не то, что хотелось бы получить в итоге, я решил проанализировать исходник функции gtk_combo_box_text_insert. Вот он. Для нас представляют интерес 501, 504 и 519 строки. После небольшой доработки, получаем интересующий нас код.
GtkTreeIter iter;
GtkListStore* store =
GTK_LIST_STORE(gtk_combo_box_get_model(GTK_COMBO_BOX(songList)));
gint text_column =
gtk_combo_box_get_entry_text_column(GTK_COMBO_BOX(songList));
gtk_combo_box_get_active_iter(GTK_COMBO_BOX(songList), &iter);
gtk_list_store_set(store, &iter, text_column, msg, -1);
Естественно, из указанных трех строчек не очевидно, что решение должно выглядеть именно так. Мне пришлось изучить описания каждой из приведенных функций на разных ресурсах, прежде чем сформировать то, что записано выше. Таким образом выяснилось, что данные для GtkComboBox хранятся в памяти в виде списка (linked list). Значит этот список нужно получить, найти указатель на выбранный элемент и заменить в нем информацию. Теперь мы можем менять текст в GtkComboBoxText, не прибегая к вызову ненужного сигнала.
Исходник
Весь исходник здесь. О том — где его полагается компилировать сказано в разделе «каркас«.
Добавить комментарий