本项目来自课设作业,要求是用GTK+做一个Linux系统下的音乐播放器,由于学习到的东西比较多,所以记录一下。
配置环境
我用的是Ubuntu20,下好之后先配置了C/C++、GTK、MakeFile、SSH的环境。
因为我写代码是在Windows上的VScode上写的,所以要用SSH来远程访问Ubuntu系统,保证写好的文件能传过来,如果嫌麻烦的话,可以直接在ubuntu系统中用vim来写代码,我认为是没vscode好用的。
MakeFile的作用其实是节省力气,它有两个好处,一个是可以编译含义多个.c和.h文件的整个工程,另一个是在命令行输入时,不用写一串很长的命令,简简单单一个make就好了,这个也看个人选择。
还有一个就是输入法,如果要打中文的话,这个也是必备的。
思路
1.先用GTK写一个GUI界面
2.再通过GUI界面的按钮触发信号,实现各种音乐播放器的功能。
3.播放功能,这个是最重要的功能,我的想法是GTK的回调函数触发后,就再进入一个封装好的音乐播放函数,在这个音乐播放函数内,实现对播放歌曲的选择,以及线程的创建(使得不会产生冲突)
4.切歌功能,这个功能和播放功能差不多,就是先暂停,换首歌继续播放。
5.暂停功能,这个我用的是杀死进程,不是完全的暂停功能。
6.搜索功能,这个我用的就是C语言的字符串的匹配函数,都是库函数里面自带的,模糊搜索也是用的自带的函数。思路就是在文本输入框内输入完内容之后,按下回车,然后有个字符串会接收输入的内容,将其和歌曲名进行比较,进而实现搜索功能。
总的来说,这个项目还是挺简单的,只要把相关的知识学好,写出来还是很容易。
代码
如果思路看不懂,也可以跟着代码看看,按逻辑展示的。
先是一些头文件和变量的声明
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>
#include <sys/wait.h>
#include <gtk/gtk.h>
#include <fcntl.h>
#include <glib.h>
#include <ctype.h>
#include <errno.h>
#include <pthread.h>
GtkWidget *window;
GtkWidget *lyricLabel;
GtkWidget *searchEntry;
GtkWidget* fixed;
gboolean search_correct;
GtkWidget *drawArea;
GtkWidget *songnameLabel;
GtkWidget* button_draw;
GtkWidget* button_stop;
GtkWidget* button_start;
GtkWidget* button_next;
GtkWidget* button_search;
GtkWidget* button_prev;
GtkWidget *progressBar;
// 歌词节点结构体
typedef struct LyricNode
{
float time; // 歌词对应的时间点
char lyric[256]; // 歌词内容
struct LyricNode *prev; // 指向前一个歌词节点的指针
struct LyricNode *next; // 指向下一个歌词节点的指针
} LYRIC_NODE;
typedef struct Song {
const char* name; // 歌曲名
const char* filePath; // 文件路径
} Song;
typedef struct Lyrics {
const char* name; // 歌词名
const char* filePath; // 文件路径
} Lyrics;
Song songList[] = {
{"加州旅馆-Eagles", "../resourcs/music/加州旅馆-Eagles.128.mp3"},
{"We-Can’t-Stop","../resourcs/music/We-Can’t-Stop-Miley.128.mp3"},
{"We-Don’t-Talk-Anymore","../resourcs/music/We-Don’t-Talk-Anymore-Charlie.mp3"}
// 添加其他歌曲信息
};
Lyrics lyricsList[] = {
{"加州旅馆 - Eagles", "../resourcs/Lyirc/加州旅馆-Eagles.lrc"},
{"We Can’t Stop","../resourcs/Lyirc/We-Cant-Stop-Miley-Cyrus.lrc"},
{"We Don’t Talk Anymore","../resourcs/Lyirc/we-dont-talk-anymore-feat-selena-gomez.lrc"}
// 添加其他歌曲信息
};
const char* pix[]={
{"../resourcs/Image/JZHotel.jpg"},
{"../resourcs/Image/We-can`t-stop.jpg"},
{"../resourcs/Image/We-don`t--talk-anymore.jpg"}
};
int numSongs = sizeof(songList) / sizeof(songList[0]); // 歌曲数量
int currentSongIndex = 0; // 当前播放的歌曲索引
pthread_t playerThread;
main函数
int main()
{
gtk_gui();
return 0;
}
GUI显示界面函数
void gtk_gui()
{
gtk_init(NULL,NULL);
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title((GtkWindow *)window,"Music Player"); //设置窗口标题
gtk_window_set_default_size((GtkWindow *)window,600,800); //设置窗口大小
gtk_window_set_resizable((GtkWindow*)window,TRUE);
gtk_window_set_position((GtkWindow *)window,GTK_WIN_POS_CENTER_ALWAYS); //设置窗口位置
g_signal_connect((GtkWindow*)window,"destroy",G_CALLBACK(deal_press_close),NULL);
//创建一个固定布局容器
fixed = gtk_fixed_new();
gtk_container_add((GtkContainer*)window,fixed); //将容器放进窗口中
//进度条
progressBar = gtk_progress_bar_new();
gtk_widget_set_size_request(progressBar, 750, 1);
gtk_fixed_put(GTK_FIXED(fixed), progressBar, 50, 600);
//按钮
button_start = gtk_button_new(); //设置一个按钮
button_next = gtk_button_new(); //设置一个按钮
button_prev = gtk_button_new(); //设置一个按钮
button_search = gtk_button_new(); //设置一个按钮
button_stop = gtk_button_new(); //设置一个按钮
button_draw = gtk_button_new(); //设置一个按钮
//设置按钮大小
gtk_widget_set_size_request(button_start,30,30);
gtk_widget_set_size_request(button_next,30,30);
gtk_widget_set_size_request(button_prev,30,30);
gtk_widget_set_size_request(button_search,30,30);
gtk_widget_set_size_request(button_stop,30,30);
gtk_widget_set_size_request(button_draw,300,300);
//让按钮背景透明
gtk_button_set_relief(GTK_BUTTON(button_start),GTK_RELIEF_NONE);
gtk_button_set_relief(GTK_BUTTON(button_next),GTK_RELIEF_NONE);
gtk_button_set_relief(GTK_BUTTON(button_prev),GTK_RELIEF_NONE);
gtk_button_set_relief(GTK_BUTTON(button_search),GTK_RELIEF_NONE);
gtk_button_set_relief(GTK_BUTTON(button_stop),GTK_RELIEF_NONE);
gtk_button_set_relief(GTK_BUTTON(button_draw),GTK_RELIEF_NONE);
//按钮显示图标
gtk_button_set_always_show_image(GTK_BUTTON(button_start), TRUE);
gtk_button_set_always_show_image(GTK_BUTTON(button_next), TRUE);
gtk_button_set_always_show_image(GTK_BUTTON(button_prev), TRUE);
gtk_button_set_always_show_image(GTK_BUTTON(button_search), TRUE);
gtk_button_set_always_show_image(GTK_BUTTON(button_stop), TRUE);
gtk_button_set_always_show_image(GTK_BUTTON(button_draw), TRUE);
//设置按钮图标
gtk_button_set_image(GTK_BUTTON(button_start), gtk_image_new_from_file("../resourcs/Image/start.png"));
gtk_button_set_image(GTK_BUTTON(button_next), gtk_image_new_from_file("../resourcs/Image/next.png"));
gtk_button_set_image(GTK_BUTTON(button_prev), gtk_image_new_from_file("../resourcs/Image/prev.png"));
gtk_button_set_image(GTK_BUTTON(button_search), gtk_image_new_from_file("../resourcs/Image/search.png"));
gtk_button_set_image(GTK_BUTTON(button_stop), gtk_image_new_from_file("../resourcs/Image/stop.png"));
g_signal_connect(button_start,"pressed",G_CALLBACK(deal_press_start),NULL);
g_signal_connect(button_next,"pressed",G_CALLBACK(deal_press_next),NULL);
g_signal_connect(button_prev,"pressed",G_CALLBACK(deal_press_prev),NULL);
g_signal_connect(button_search,"pressed",G_CALLBACK(deal_press_search),NULL);
g_signal_connect(button_stop,"pressed",G_CALLBACK(deal_press_stop),NULL);
//添加按钮到fixed中
gtk_fixed_put(GTK_FIXED(fixed), button_start, 270, 650);
gtk_fixed_put(GTK_FIXED(fixed), button_stop, 360, 650);
gtk_fixed_put(GTK_FIXED(fixed), button_next, 450, 650);
gtk_fixed_put(GTK_FIXED(fixed), button_prev, 180, 650);
gtk_fixed_put(GTK_FIXED(fixed), button_search, 600, 20);
gtk_fixed_put(GTK_FIXED(fixed), button_draw, 120, 100);
//歌曲名显示
songnameLabel = gtk_label_new("");
gtk_widget_set_size_request(songnameLabel, 700, 50);
gtk_label_set_justify(GTK_LABEL(songnameLabel), GTK_JUSTIFY_CENTER);
GtkCssProvider *songnameCss = gtk_css_provider_new();
gtk_css_provider_load_from_data(songnameCss,
".name{color: #000000; font-size: 32px;}",
59, NULL);
GtkStyleContext *songnameStyle = gtk_widget_get_style_context(songnameLabel);
gtk_style_context_add_class(songnameStyle, "name");
gtk_style_context_add_provider(songnameStyle, GTK_STYLE_PROVIDER(songnameCss), GTK_STYLE_PROVIDER_PRIORITY_FALLBACK);
g_object_unref(songnameCss);
gtk_label_set_single_line_mode(GTK_LABEL(songnameLabel), TRUE);
gtk_label_set_selectable(GTK_LABEL(songnameLabel), FALSE);
gtk_fixed_put(GTK_FIXED(fixed), songnameLabel, 200, 60);
//歌词框
// 创建一个空的标签
lyricLabel = gtk_label_new("");
// 设置标签的大小
gtk_widget_set_size_request(lyricLabel, 700, 50);
// 设置标签文本居中显示
gtk_label_set_justify(GTK_LABEL(lyricLabel), GTK_JUSTIFY_CENTER);
// 创建一个 CSS 提供者对象
GtkCssProvider *labelCss = gtk_css_provider_new();
// 从数据加载 CSS 样式,包括字体和颜色
gtk_css_provider_load_from_data(labelCss, ".lyric{color: #FF0000; font-size: 32px;}", -1, NULL);
// 获取标签的样式上下文
GtkStyleContext *lyricLabelStyle = gtk_widget_get_style_context(lyricLabel);
// 添加样式类
gtk_style_context_add_class(lyricLabelStyle, "lyric");
// 将 CSS 提供者添加到标签的样式上下文中
gtk_style_context_add_provider(lyricLabelStyle, GTK_STYLE_PROVIDER(labelCss), GTK_STYLE_PROVIDER_PRIORITY_FALLBACK);
// 释放 CSS 提供者对象
g_object_unref(labelCss);
// 设置标签为单行模式
gtk_label_set_single_line_mode(GTK_LABEL(lyricLabel), TRUE);
// 设置标签不可选择
gtk_label_set_selectable(GTK_LABEL(lyricLabel), FALSE);
// 将标签放置在固定容器中的指定位置
gtk_fixed_put(GTK_FIXED(fixed), lyricLabel, 80, 550);
searchEntry = gtk_entry_new();
gtk_entry_set_text((GtkEntry*)searchEntry,"请输入歌名:");
gtk_editable_set_editable((GtkEditable*)searchEntry,TRUE);
gtk_fixed_put(GTK_FIXED(fixed),(GtkWidget*)searchEntry, 400, 20);
g_signal_connect(searchEntry, "activate", G_CALLBACK(enter_callback), searchEntry);
gtk_widget_show_all((GtkWidget*)window); //显示窗口所有的控件
gtk_main();
}
按下按钮,触发回调函数,先是播放函数
void deal_press_start(GtkButton* button)
{
playSong(); // 播放歌曲
return;
}
接下来是切歌、暂停和搜索函数
void deal_press_next(GtkButton* button)
{
stopSong(); // 停止当前播放的歌曲
currentSongIndex = (currentSongIndex + 1) % numSongs; // 获取下一首歌曲的索引
playSong(); // 播放下一首歌曲
return;
}
void deal_press_prev(GtkButton* button)
{
stopSong(); // 停止当前播放的歌曲
currentSongIndex = (currentSongIndex + 2) % numSongs; // 获取上一首歌曲的索引
playSong(); // 播放上一首歌曲
return;
}
void deal_press_search(GtkButton* button)
{
stopSong(); // 停止当前播放的歌曲
if(search_correct==TRUE)
{
playSong(); // 播放歌曲
}
return;
}
void deal_press_stop(GtkButton* button)
{
stopSong();
return;
}
接着来播放函数,这里将其封装好的原因是,切歌和搜索后都会用到,所以为了代码的精简,将这三句代码封装。
void playSong()
{
gtk_label_set_text((GtkLabel*)songnameLabel, songList[currentSongIndex].name);
gtk_button_set_image(GTK_BUTTON(button_draw), gtk_image_new_from_file(pix[currentSongIndex]));
pthread_create(&playerThread, NULL, mplayer_thread, NULL);
}
按流程,进入歌曲播放的真正函数,这里的参数是必须有的,但用不着。
void* mplayer_thread(void* arg)
{
const char* lyricfilePath=(const char*)lyricsList[currentSongIndex].filePath;
char lyriccommand[256];
sprintf(lyriccommand, "%s", lyricfilePath);
FILE *lrcFile = fopen(lyriccommand, "r"); // 打开歌词文件
if (lrcFile == NULL)
{
perror("Error opening lyrics file");
return NULL;
}
LYRIC_NODE *lyricList = parseLrc(lrcFile); // 解析lrc歌词格式并存储到双向链表中
fclose(lrcFile); // 关闭歌词文件
const char* songfilePath = (const char*)songList[currentSongIndex].filePath;
char songcommand[256];
sprintf(songcommand, "mplayer %s", songfilePath);
FILE *pipe = popen(songcommand, "r"); // 使用mplayer播放音乐,并获取输出流
if (pipe == NULL)
{
perror("Error opening pipe");
return NULL;
}
char buffer[256];
while (fgets(buffer, sizeof(buffer), pipe) != NULL)
{
float time;
sscanf(buffer, "A: %f", &time); // 从mplayer输出中解析当前播放时间
LYRIC_NODE *current = lyricList;
while (current != NULL)
{
if (current->time > time)
break;
const char* lyrics = current->lyric;
char* lyrics_copy = g_strdup(lyrics); // 复制歌词字符串
g_idle_add(update_lyrics_in_gui, lyrics_copy); // 使用g_idle_add异步调用更新 GUI 上的歌词标签
current = current->next;
}
}
pclose(pipe); // 关闭mplayer输出流
freeLyricList(lyricList); // 释放双向链表内存
return NULL;
}
这段代码里面的歌词解析函数如下,目的是按行将歌词内容和时间给到双向链表,以便后续歌词和歌曲播放对应的上。
LYRIC_NODE *parseLrc(FILE *file)
{
LYRIC_NODE *head = NULL; // 头节点指针
LYRIC_NODE *tail = NULL; // 尾节点指针
char line[256]; // 用于存储每行歌词内容
// 从文件中逐行读取数据,直到文件末尾
while (fgets(line, sizeof(line), file) != NULL)
{
int min, sec, msec; // 分、秒、毫秒
// 获取歌词内容,跳过时间标记部分
char *lyric = strchr(line, ']') + 1;
// 如果成功解析出时间点信息
if (sscanf(line, "[%d:%d.%d", &min, &sec, &msec) == 3)
{
// 计算时间点的总秒数
float time = min * 60.0 + sec + msec / 1000.0;
// 创建新的歌词节点
LYRIC_NODE *newNode = (LYRIC_NODE *)malloc(sizeof(LYRIC_NODE));
// 将时间点和歌词内容存储到新节点中
newNode->time = time;
strcpy(newNode->lyric, lyric);
newNode->prev = NULL;
newNode->next = NULL;
// 如果链表为空,将新节点设置为头尾节点
if (head == NULL)
{
head = newNode;
tail = newNode;
} else
{ // 如果链表不为空,将新节点添加到链表末尾
tail->next = newNode;
newNode->prev = tail;
tail = newNode;
}
}
}
return head; // 返回头节点指针
}
这是释放内存的,不用多说
void freeLyricList(LYRIC_NODE *head)
{
LYRIC_NODE *current=(LYRIC_NODE*)malloc(sizeof(head));
current = head;
while (current != NULL)
{
LYRIC_NODE *temp = current;
current = current->next;
free(temp);
}
}
回到播放歌曲和歌词的那个函数,里面有个异步调用,更新歌词,它调用的函数会如下,目的是将歌词更新
gboolean update_lyrics_in_gui(gpointer data)
{
gtk_label_set_text((GtkLabel*)lyricLabel, (const char*)data);
return G_SOURCE_REMOVE; //回调函数被成功移除,资源释放
}
播放歌曲的功能就说完了,切歌的差不多,因为我是用数组进行歌曲更换的,所以就是要注意数组下标的变化
接着是暂停的函数,因为也是多处要用,所以将其封装
void stopSong()
{
pthread_cancel(playerThread);
int ret=system("killall -9 mplayer");
if(ret<0)
{
perror("kill error!\n");
return;
}
}
搜索函数如下,在GTK中,输入文本后,要获取内容,再将其进行比较,如果文本长度是等于歌曲长度,就是正常搜索,否则就是模糊搜索。
void enter_callback(gpointer entry )
{
const char *entry_text;
// 获得文本内容
entry_text = gtk_entry_get_text(GTK_ENTRY(entry));
search_correct=search_song_name(entry_text);
}
gboolean search_song_name(const char* entry_name)
{
int entry_lenth=strlen(entry_name);
int index=numSongs;
while(index)
{
int song_name_lenth=strlen(songList[index-1].name);
if(song_name_lenth==entry_lenth)
{
if(strcmp(songList[index-1].name,entry_name)==0)
{
printf("find sucess!\n");
currentSongIndex=index-1;
return TRUE;
}
}
else if(song_name_lenth>entry_lenth)
{
char* judge_index=strstr((char*)songList[index-1].name,entry_name);
if(judge_index!=NULL)
{
printf("find sucess!\n");
currentSongIndex=index-1;
return TRUE;
}
}
index--;
}
printf("find error!\n");
return FALSE;
}
大致的代码就是这些。
后言
其实本项目还有很多可以完善的地方,比如说那个暂停功能,还有歌词的解析,可以将普通文本格式的歌词解析为irc格式的歌词,还有许多地方和不足,欢迎大家指出。