基于Linux系统的音乐播放器

本项目来自课设作业,要求是用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格式的歌词,还有许多地方和不足,欢迎大家指出。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值