音频分析 Python程序 开发中
内容目录

说明

一,背景

本项目起于研一上学期专业课的学期任务——用Python编写一个简易的音频播放程序,实现基本的音频播放、波形显示等功能。制作过程中,这一程序的功能目标逐渐转向了对于音频的分析及其可视化,播放只是附带。目前的阶段功能比较基础,持续开发中。

GUI库的选择 - 初步尝试了Tkinter、PyGame后,最终定在了第三个选择上:PyQt。三种工具库都是目前Python环境下的主流GUI库。Tkinter由于已纳入Python标准库,有其方便、简易等优点,但它的功能非常局限,只能用于最基本的UI设计而缺乏数据处理方面的函数。PyGame作为python环境下游戏开发最主要的库,功能全面、可实现音视频播放等功能,但由于时间紧迫,且有关的学习资源都侧重游戏开发,因此暂且放弃。PyQt是Qt框架的Python绑定,Qt最初是作为C++的跨平台应用程序开发框架而设计的。作为一个相对历史比较悠久的C++库,它的功能相当丰富,但也十分复杂、庞大。宏观的模块就有约五十多种,最常用的模块中,每种都包含了上百个不同的类,每个类中又含有几十至上百个函数。这一工具库的庞杂使得学习的门槛较高,其内部逻辑也并不完美,很多类名和函数名非常近似、逻辑关系不明晰,难以理解。在长久的迭代过程中,一代代的版本更新和函数新增使得这个库变得越来越繁琐。尽管如此,在Python环境下,对于一般功能的程序开发(除了游戏),个人认为PyQt是最佳选择。Qt官方提供了便捷实用的Qt Creator,这一程序套件中包含的Qt Designer是一个可视化UI设计程序,方便开发人员高效完成GUI层面的代码。
目前Qt已发展到第六代,本项目使用的是PyQt的第五代成熟版本。唯一的缺点是,PyQt非原生Python库,只是Qt的一种“延伸”,Qt官方工具基于C++进行转译,实际工作中需要面对一些C++文件,这对于不熟悉C的用户比较不友好。而且,Qt Creator似乎很多年没有更新它的Qt Creator软件,在当下的软硬件环境中有部分隐性的兼容问题,Qt自带的UI图标库也已经是近二十年前的审美风格(当然对于专业开发人员,美术设计都是自定义的)。

二,目标需求

除了基本的音频播放,这个程序的开发更多是为了与“面向音乐的音频信号处理”的学习的自然衔接。
软件的基本需求可以整理为如下几项:
调取程序对话框、文件选择
播放音频,可调整音量、调整进度条等
音频文件的基本信息显示(文件名、时长、采样率)
音频文件的波形
音频文件的频谱图
基本波形图的其他信息(面向包络分析的起音阶段、音频的RMS计算等)

三,程序设计

简单的IPO图:

基本功能与处理流程

数据的起始点为用户已操作“文件选择”后,在这之前程序执行对系统对话框的调取,用于读取磁盘存储中的文件。待用户选择格式正确的音频文件后,程序将执行左图所示的一系列数据的计算和处理。

获取文件路径 - 获取音频全部采样信息 - 通过计算,获得时长等信息并输出到GUI - 调取系统播放器 - 计算&绘制波形图 - FFT&绘制频谱

其他变化型波形图:在基本波形图的基础上增加其他计算(RMS、Peak等)或截取所需的片段(时域截取)

四,程序实现

基于OOP编程思路,主要用到如下库:
GUI - PyQt5;
数据处理、科学计算库 - Numpy、Scipy
绘图 - Matplotlib(基本的pyplot和专用于PyQt GUI的Qt5后端模块 FigureCanvasQtAgg)
系统音频处理 - 标准库 subprocess
系统响应 - 标准库 sys、os

代码

主窗口对应三个.py文件,后来又增加了帮助窗口,因此多了两个.py文件(一个窗口模块,一个UI,无其他函数需求,只有UI)。帮助窗口很简单,基本逻辑和主窗口一致,就不复制在这里了。

三个关键的python文件分别对应主程序、主窗口、ui,是互相调用的关系,最底层的是ui:

# -*- coding: utf-8 -*-

from PyQt5 import QtCore, QtWidgets #,QtGui
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas  #专门用于qt的canvas模块

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):     # MainWindow作为入参,the greatest parent
        MainWindow.setObjectName("MainWindow")        
        window_width = 720
        window_height = 550
        MainWindow.resize(window_width, window_height)
        desktop = QtWidgets.QDesktopWidget()            #建立桌面实例
        my_scr = desktop.screenGeometry()               #当下显示器屏幕尺寸
        MainWindow.move(my_scr.left(),my_scr.top())

        self.menuBar = QtWidgets.QMenuBar(MainWindow)
        self.menuBar.setGeometry(QtCore.QRect(0, 0, 743, 23))
        self.menu_File = QtWidgets.QMenu(self.menuBar)        #一级目录 
        self.menu_Contrl = QtWidgets.QMenu(self.menuBar)  
        self.menu_Help = QtWidgets.QMenu(self.menuBar)    
        MainWindow.setMenuBar(self.menuBar)    
        self.menuBar.addAction(self.menu_File.menuAction())   #menu对象(menuaction)加入到menubar中
        self.menuBar.addAction(self.menu_Contrl.menuAction())
        self.menuBar.addAction(self.menu_Help.menuAction())

        self.actFile_Open = QtWidgets.QAction(MainWindow)     #建立QAction对象 二级目录
        self.actWin_Close = QtWidgets.QAction(MainWindow)
        self.actMedia_play = QtWidgets.QAction(MainWindow)
        # self.actMedia_stop = QtWidgets.QAction(MainWindow)
        self.actUserGuide = QtWidgets.QAction(MainWindow)

        self.menu_File.addAction(self.actFile_Open)      #把二级目录加入到一级目录中
        self.menu_File.addAction(self.actWin_Close)
        self.menu_Contrl.addAction(self.actMedia_play)
        # self.menu_Contrl.addAction(self.actMedia_stop)
        self.menu_Help.addAction(self.actUserGuide)

        self.figure1 = plt.figure(figsize=(9,4))       #建立图表对象
        self.figure1.tight_layout()
        self.canvas1 = FigureCanvas(self.figure1) #建立qt框架内的FigureCanvas对象
        self.canvas1.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding,)

        self.centralWidget = QtWidgets.QWidget(MainWindow)
        self.centralWidget.setObjectName("centralWidget")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralWidget)
        self.verticalLayout_2.setContentsMargins(1, 1, 1, 1)
        self.verticalLayout_2.setSpacing(1)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.groupBox = QtWidgets.QGroupBox(self.centralWidget)
        self.groupBox.setObjectName("groupBox")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.groupBox)
        self.verticalLayout.setContentsMargins(2, 2, 2, 2)
        self.verticalLayout.setSpacing(2)
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_3.setSpacing(6)
        self.horizontalLayout_3.setObjectName("horizontalLayout_3")

        #plot buttons
        self.btnWavePlot = QtWidgets.QPushButton(self.groupBox)
        self.btnWavePlot.setObjectName("btnWavePlot")
        self.horizontalLayout_3.addWidget(self.btnWavePlot)
        self.btnADSRPlot = QtWidgets.QPushButton(self.groupBox)
        self.btnADSRPlot.setObjectName("btnADSRPlot")
        self.horizontalLayout_3.addWidget(self.btnADSRPlot)
        self.btnSpecPlot = QtWidgets.QPushButton(self.groupBox)
        self.btnSpecPlot.setObjectName("btnSpecPlot")
        self.horizontalLayout_3.addWidget(self.btnSpecPlot)
        self.btnPeakRMS = QtWidgets.QPushButton(self.groupBox)
        self.btnPeakRMS.setObjectName("btnPeakRMS")
        self.horizontalLayout_3.addWidget(self.btnPeakRMS)
        self.verticalLayout.addLayout(self.horizontalLayout_3)
        self.mediaInfoLab = QtWidgets.QLabel(self.groupBox)

        #按窗口宽度计算label宽度:
        self.mediaInfoLab.setMinimumSize(QtCore.QSize(int(window_width*0.9),60))
        self.verticalLayout.addWidget(self.mediaInfoLab)
        self.verticalLayout.addWidget(self.canvas1)   #canvas加入到外层layout中
        # self.verticalLayout.addWidget(self.canvas2)         
        self.verticalLayout_2.addWidget(self.groupBox)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setSpacing(6)
        self.horizontalLayout.setObjectName("horizontalLayout")

        self.btnOpen = QtWidgets.QPushButton(self.centralWidget)    #open button
        self.btnOpen.setFixedSize(60,30)
        self.btnOpen.setText("open")
        self.btnOpen.setObjectName("btnOpen")
        self.horizontalLayout.addWidget(self.btnOpen)

        self.btnPlay = QtWidgets.QPushButton(self.centralWidget)   #play button
        self.btnPlay.setFixedSize(60,30)
        self.btnPlay.setText("play")
        self.btnPlay.setObjectName("btnPlay")
        self.horizontalLayout.addWidget(self.btnPlay)        
        self.verticalLayout_2.addLayout(self.horizontalLayout)
        self.line = QtWidgets.QFrame(self.centralWidget)
        self.line.setFrameShadow(QtWidgets.QFrame.Plain)
        self.line.setFrameShape(QtWidgets.QFrame.HLine)
        self.line.setObjectName("line")
        self.verticalLayout_2.addWidget(self.line)
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setSpacing(9)
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.verticalLayout_2.addLayout(self.horizontalLayout_2)
        MainWindow.setCentralWidget(self.centralWidget)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Basic Audio Displayer"))
        self.groupBox.setTitle(_translate("MainWindow", "An audio displayer written by Zhang Hong"))
        self.btnWavePlot.setText(_translate("MainWindow", "waveform"))
        self.btnSpecPlot.setText(_translate("MainWindow", "spectrums"))
        self.btnPeakRMS.setText(_translate("MainWindow", "Peak and RMS"))
        self.btnADSRPlot.setText(_translate("MainWindow", "portion plot"))
        self.menu_File.setTitle(_translate("MainWindow", "File"))
        self.menu_Contrl.setTitle(_translate("MainWindow", "Control"))
        self.menu_Help.setTitle(_translate("MainWindow", "Help"))
        self.actFile_Open.setText(_translate("MainWindow", "Open"))
        self.actWin_Close.setText(_translate("MainWindow", "Close"))
        self.actMedia_play.setText(_translate("MainWindow", "Play"))
        # self.actMedia_stop.setText(_translate("MainWindow", "Stop"))
        self.actUserGuide.setText(_translate("MainWindow", "User Guide"))

一般来讲,这部分代码不会有人手工去打的,都是用QT Creator进行可视化设计,然后自动生成。由于我电脑里的Creator始终配置失败,所以我勉强复制到一部分QT教材里提供的模版,然后在其基础上手工修改,命名规则遵循自动生成的那些规律。

最关键、最重要的,也是最需要自己编程的模块则是主窗口模块:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Mon Nov 27 20:36:35 2023

@author: Sylvia
"""

import sys, os
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog #, QListWidgetItem
from PyQt5.QtCore import QUrl, QDir, QFileInfo, Qt    #QModelIndex,pyqtSlot, 
from PyQt5.QtMultimedia import QMediaPlayer, QMediaPlaylist, QMediaContent
import matplotlib.pyplot as plt
import numpy as np
import scipy.io.wavfile as wv

from ui_MainWindow import Ui_MainWindow
from UserGuide import UserGuideWindow
import subprocess

class MyMainWindow(QMainWindow):        #主窗体
    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui=Ui_MainWindow()    #构建窗体的ui
        self.ui.setupUi(self)      #建立GUI界面
        self.player = QMediaPlayer(self)
        self.playlist = QMediaPlaylist(self)
        self.__duration=""
        self.__curPos=""

        #自定义ui中的menu, trigger信号与槽连接
        self.ui.actFile_Open.triggered.connect(self.File_Open_triggered)
        self.ui.actWin_Close.triggered.connect(self.Win_Close_triggered)
        self.ui.actMedia_play.triggered.connect(self.Media_play_triggered)
        self.ui.actUserGuide.triggered.connect(self.UserGuide_triggered)
        self.x_axis = None
        self.y_axis = None
        self.y_data = None
        self.whole_path = None
        self.media = None
        self.data = None
        self.sr = None
        self.dur = None
        self.warningInfo = "PLEASE OPEN A FILE FIRST!"

   #===================== 自定义trigger槽 ===================================

    def File_Open_triggered(self):
        print('【检测程序】the function File_Open_triggered is running')
        curPath = QDir.currentPath()             #os.gtcwd()
        dlgTitle = "choose an audio file"
        filt = "音频文件(*.mp3 *.wav *.wma)"
        filename, _ = QFileDialog.getOpenFileName(self,dlgTitle,curPath,filt)

        fileInfo = QFileInfo(filename)         #获取用户选择文件的信息(建立QFileInfo对象)
        folder_path = fileInfo.absolutePath()  #获取文件夹绝对路径
        #fileInfo.baseName()                   #basename without the path
        file_name = os.path.basename(filename)
        whole_path = folder_path+"/"+file_name
        QDir.setCurrent(folder_path)            
        self.whole_path = whole_path
        song = QMediaContent(QUrl.fromLocalFile(filename)) #建立mediacontent对象并加入medialist以及用于绘制
        self.playlist.addMedia(song)
        self.media = song                                   
        # s_name = fileInfo.baseName()  #文件名 无后缀

        try:
            __sr,__y = wv.read(whole_path)  #读取音频数据
        except:
            warning_text = "WARNING: data reading support for wav file only"
            self.ui.mediaInfoLab.setText(warning_text)
            pass
        else:
            self.playlist.clear()
            self.ui.mediaInfoLab.clear()
            if __y.ndim != 1:
                y_mean = np.mean(__y,axis=1,dtype=__y.dtype) #转为一维
                bit_dep = int(str(y_mean.dtype)[-2:])  #调取bit深度用以计算量化阶数
                __y = y_mean/(2**bit_dep/2)                  #归一化

            #用sampling rate计算时长 
            dur_sec = __y.size // __sr             #秒数取整
            dur_milisec = (__y.size % __sr)/__sr  #浮点数
            __dur = dur_sec+dur_milisec           #original duration data
            self.dur = __dur
            self.data = __y  
            self.sr = __sr
            dur_milisec_show = str(dur_milisec)[2:5]        #小数点后三位(文本)
            dur_show = "media duration: "+str(dur_sec)+dur_milisec_show+"ms"+".  "
            sr_show = "sampling rate: "+str(__sr)+"."
            tip_show="Displaying waveform up to 2.0 seconds and two spectrums below 6 kHz and 2 kHz respectively."
            infotext="--- "+file_name+" ---\n-> "+dur_show+sr_show+"\n-> "+tip_show
            self.ui.mediaInfoLab.setText(infotext)
            self.plot_waveform()

    #set x/y axis data for waveform plots
    def set_waveplot_xy(self):   
        if self.dur>2.0:                                        # 截取2000ms时长
            self.x_axis = np.linspace(0,2.0,num=self.sr*2)  
            trunc_data = self.data[:self.sr*2]                  #原数据切片
            self.y_data = trunc_data/np.max(np.abs(trunc_data)) #振幅归一化 用于计算
            self.y_axis = np.round(self.y_data,3)               #用于实际显示
        else:
            self.x_axis = np.linspace(0,self.dur,self.data.size)  #实际时长实际样点数
            self.y_data = self.data/np.max(np.abs(self.data))     #实际data 未舍入 用于后续计算
            self.y_axis = np.round(self.y_data,3)

    def plot_waveform(self):
        self.set_waveplot_xy()
        self.ui.figure1.clear()
        print("【检测程序】figure1 clearing......")
        fig = self.ui.figure1.add_subplot(111)
        fig.clear()
        fig.plot(self.x_axis,self.y_axis,color='#00AEAE',linestyle='-',linewidth=0.3)
        print("【检测程序】figure1 plotting......")
        fig.set_xlabel('Time(s)')
        fig.set_ylabel('Amp')
        fig.set_title("Audio waveform")
        fig.grid(True) 
        self.ui.canvas1.draw()            

    def ADSR_plot(self):
        if len(self.data) >= (self.sr*0.2):   #数据长度不小于200ms 
            pointA = int(0.1*self.sr)        #十分之一采样率作为节点
            attack_data = self.y_axis[:pointA]             # 前100ms
            sustain_data = self.y_axis[pointA:pointA*2]    # 100~200ms
            self.ui.figure1.clear()        
            fig1 = self.ui.figure1.add_subplot(211)
            fig1.clear()
            fig1_x = np.linspace(0,self.dur*0.1,num=pointA)
            fig1.plot(fig1_x,attack_data,color='#35ACD9',linestyle='-',linewidth=1)
            fig1.set_xlabel('Time(s)')
            fig1.set_ylabel('Amp')
            fig1.set_title("attack phase")
            fig1.grid(True) 
            fig2 = self.ui.figure1.add_subplot(212)
            fig2.clear()
            fig2_x = np.linspace(self.dur*0.1, self.dur*0.2, num=pointA)
            fig2.plot(fig2_x,sustain_data,color='#35ACD9',linestyle='-',linewidth=1)
            fig2.set_xlabel('Time(s)')
            fig2.set_ylabel('Amp')
            fig2.set_title("sustain phase")
            fig2.grid(True) 
            self.ui.figure1.subplots_adjust(hspace=0.4)
            self.ui.canvas1.draw() 

        elif len(self.data)0)     #去掉负值部分
        freq = freq[mask]                     
        X_abs = X_abs[mask]
        freq1 = freq[freq<=6000]       #过滤6k赫兹以上的部分
        X_abs1 = X_abs[:len(freq1)]
        X_normalized1 = X_abs1/np.max(X_abs)
        freq2 = freq[freq<=2000]       #过滤2k赫兹以上的部分
        X_abs2 = X_abs[:len(freq2)]
        X_normalized2 = X_abs2/np.max(X_abs)

        self.ui.figure1.clear()
        fig1 = self.ui.figure1.add_subplot(211)
        fig2 = self.ui.figure1.add_subplot(212)
        fig1.clear()
        fig2.clear()
        fig1.plot(freq1,X_normalized1,color='orange',linewidth=0.9)
        fig1.set_xlabel('Freq(Hz)')
        fig1.set_ylabel('Magnitude')
        fig1.set_title("spectrum under 6kHz")
        fig1.grid(True)
        fig2.plot(freq2,X_normalized2,color='orange',linewidth=0.9)
        fig2.set_xlabel('Freq(Hz)')
        fig2.set_ylabel('Magnitude')
        fig2.set_title("spectrum under 2kHz")
        fig2.grid(True)
        self.ui.figure1.subplots_adjust(hspace=0.4)        
        self.ui.canvas1.draw()

    def plot_peak_RMS(self):
        self.set_waveplot_xy()
        self.ui.figure1.clear()        
        fig = self.ui.figure1.add_subplot(111)
        fig.clear()
        fig.plot(self.x_axis,self.y_axis,color='pink',linewidth=0.3,alpha=0.8)
        fig.set_xlabel('Time(s)')
        fig.set_ylabel('Amp')
        fig.set_title("peak and RMS")
        fig.grid(True) 
        peak_value = np.max(np.abs(self.y_axis))
        rms_value = np.sqrt(np.mean(self.y_data**2))
        plt.axhline(y=peak_value,linestyle='--',linewidth=1.2,color='r',label='Peak')
        plt.axhline(y=rms_value,linestyle='--',linewidth=1.2,color='b',label='RMS')
        plt.legend()
        self.ui.canvas1.draw()

    def sys_mediaplay(self):
        subprocess.run(["open",self.whole_path])

    def UserGuide_triggered(self):
        help_win = UserGuideWindow(self)
        help_win.setWindowFlag(Qt.Window, True)
        help_win.show()

    def Media_play_triggered(self):
        self.player.setMedia(self.media)      
        self.player.play()

    def Win_Close_triggered(self):
         sys.exit()

  # ======= connectSlotsByName()型函数 =============
    # @pyqtSlot()
    def on_btnWavePlot_clicked(self):
        try:
            self.plot_waveform()
        except TypeError:
            self.ui.mediaInfoLab.setText(self.warningInfo)
            pass

    def on_btnADSRPlot_clicked(self):
        try:
            self.ADSR_plot()
        except TypeError:
            self.ui.mediaInfoLab.setText(self.warningInfo)
            pass

    def on_btnSpecPlot_clicked(self):
        try:
            self.plot_spectrum()
        except Exception as e:
            self.ui.mediaInfoLab.setText(str(e)+"\n"+self.warningInfo)
            pass

    def on_btnPeakRMS_clicked(self):
        try:
            self.plot_peak_RMS()
        except Exception as e:
            self.ui.mediaInfoLab.setText(str(e)+"\n"+self.warningInfo)
            pass

    def on_btnPlay_clicked(self):
        try: 
            self.sys_mediaplay()
        except TypeError:
            warningInfo = "PLEASE OPEN A FILE FIRST!"
            self.ui.mediaInfoLab.setText(warningInfo)
            pass

    def on_btnOpen_clicked(self):   # 为什么会二次触发???
        self.File_Open_triggered()

#测试程序
if __name__ == "__main__":
    app = QApplication(sys.argv)
    form = MyMainWindow()  #创建窗体
    form.show()
    sys.exit(app.exec_())

        # curPath = QDir.currentPath()
        # dlgTitle = "choose an audio file"
        # filt = "音频文件(*.mp3 *.wav *.wma)"
        # __filename, _ = QFileDialog.getOpenFileName(self,dlgTitle,curPath,filt)
        # __fileInfo = QFileInfo(__filename)   #获取用户选择文件的信息(建立QFileInfo对象)
        # QDir.setCurrent(__fileInfo.absolutePath())  #将用户文件路径设为当前路径
        # # print(__fileInfo.absolutePath())
        # song = QMediaContent(QUrl.fromLocalFile(__filename)) 

        # __s_name = __fileInfo.baseName()  #文件名和后缀
        # self.ui.listWidget.addItem(__s_name)

  #事件处理      
    # def closeEvent(self,event):
    #     #事件函数 closeEvent(event),event是QCloseEvent类型
    #     if self.player.state()==QMediaPlayer.PlayingState:
    #         self.player.stop()

这部分直接调用UI模块,而第三个.py文件,即主程序模块则是调用这个主窗口模块,又是一个程式化的模块:

#  GUI应用程序主程序入口,创建应用程序和主窗体,显示主窗体并运行应用程序

import sys
from PyQt5.QtWidgets import  QApplication

from myMainWindow import MyMainWindow

app = QApplication(sys.argv)    #创建主程序 【Qt应用程序的主要类之一】
                                #使用sys.argv获取命令行参数,并根据这些参数的值来配置应用程序的行为。
mainform=MyMainWindow()        #创建主窗体 调用自建的类-MyMainWindow()
mainform.show()                #显示主窗体
sys.exit(app.exec_())          #.exec_():QApplication类的一个方法,用于启动应用程序的主事件循环
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇