教你轻松制作java音乐播放器

一、音乐播放器的实现原理

Javase的多媒体功能很弱,所以有一个专门处理多媒体的插件叫JMF,JMF提供的模型可大致分为七类

* 数据源(Data source)
* 截取设备(Capture Device,包括视频和音频截取设备)
* 播放器(Player)
* 处理器(Processor)
* 数据池(DataSink)
* 数据格式(Format)
* 管理器(Manager)

而我所做的这个音乐播放器MyMusicPlayer(这是我创建的类名)正是调用了JMF中的Player类来实现其播放等各种功能.

我们首先要做的就是要安装JMF。JMF的安装,相信对于许多的新手来说是很伤脑筋的,JMF只支持32位的JDK版本,然而像eclipse这样的IDE环境要与JDK对应,也就是IDE环境要支持32位JDK版本。当安装完JMF之后,有时候对于MP3的播放并不成功,还需要装JMF的mp3plugin。

二、界面效果图

三、功能结构图

四、各种实现功能的代码

public class MyMusicPlayer implements ActionListener, ControllerListener,Runnable{

 JFrame j=new JFrame("音乐播放器");
 JLabel TablePlaer=new JLabel("播放列表");
 JButton BAdd=new JButton("添加歌曲");
 JButton BDelect=new JButton("删除歌曲");
 JButton BDelectTable=new JButton("清空列表");

 JButton BMoveNext=new JButton("下一曲");
 JButton BMovePrevious=new JButton("上一曲");
 JButton BPlayer=new JButton("暂停");
 JButton BStop=new JButton("停止");
 JButton BSet=new JButton("显示歌词");
 JButton BEnd=new JButton("停止");
 String[] s={"顺序播放","单曲循环","随机播放"};        //下拉列表选项数组
 JComboBox select=new JComboBox(s);          //创建下拉选项
 JPanel p1=new JPanel();           //播放列表区域
 JPanel p=new JPanel();
 JPanel p2=new JPanel();           //按钮区域
 JPanel p3=new JPanel();
 JLabel l=new JLabel();
 JPanel p5=new JPanel(); //放置播放列表
 JPanel p6=new JPanel(); //放置播放歌曲的名称

 static JPanel pp=new JPanel();
 static JLabel lb;
 public static JTextArea jt=new JTextArea();

 static int index;  //播放列表的下标
 int count;
 int flag;   //标记是随机播放还是顺序播放
 int countSecond; //获取音乐的总时间值
 static int newtime = 0;
 int ischanging = 0; //当鼠标是对游标进行点击,进度值也会改变
 int ispressing = 0; //判断鼠标是否对游标进行点击
 File MusicName = null;
 static java.util.List<File> MusicNames = null;  //运用泛型,创建File对象
 File currentDirectory = null;
 List list;// 文件列表
 FileDialog open; // 定义文件对话框对象

 Random rand = new Random();

 String filename;

 //进度条
 JButton timeInformation = new JButton();
 JSlider timeSlider = new JSlider(SwingConstants.HORIZONTAL, 0, 100, 0); //(SwingConstants.HORIZONTAL)用于定向进度条为水平方向的常量的集合
                     //( 0, 100, 0)用指定的最小值、最大值和初始值创建一个水平滑块。

 // 播放
 Player player = null;
 MusicFileChooser fileChooser = new MusicFileChooser();

 static JTextPane tp=new JTextPane();  //显示歌词区域
 static JTextArea are=new JTextArea(); //显示图片区域

 public MyMusicPlayer(){
  j.setSize(1200, 700);
  j.setLayout(null);
  j.getContentPane().setBackground(Color.BLACK);
  j.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  p.setBounds(2, 563, 1180, 95);
  p.setLayout(new BorderLayout());

  p1.setBounds(2, 3, 298, 30);
  p1.setBackground(new Color(255,255,255));

  p2.setLayout(new GridLayout(2,3,20,20));
  p2.setBackground(Color.LIGHT_GRAY);

  p3.setLayout(new GridLayout(2,0,200,10));
  p3.setBackground(new Color(255,255,255));

  p5.setBounds(2, 35, 298, 526);
  p5.setLayout(null);
  p5.setBackground(new Color(255,255,255));

  p6.setBounds(301, 3,880, 30);
  p6.setLayout(null);
  p6.setBackground(new Color(255,255,255));

  l.setBounds(250, 4, 600, 30);  //设置显示播放的歌曲
  p6.add(l);

  /*实现图片插入
   * */
  ImageIcon ic=new ImageIcon("image\\2.3.jpg");
  ic=new ImageIcon(ic.getImage().getScaledInstance(880, 477, 2)); //获取图片以及设置图片大小

  lb=new JLabel(ic);
  lb.setOpaque(false);
  pp.setOpaque(false);  //设置为透明

  pp.setBounds(241, 80,990, 500);

  are.setBounds(241, 56,990, 520);
  are.setOpaque(false);

  tp.setBackground(new Color(255,255,255));
  tp.setBounds(301, 35,880, 49);

  pp.add(are);
  pp.add(lb);

  // 文件列表
  list = new List(10);
  list.setBounds(100, 55, 187, 495); //列表区域
  list.addActionListener(this);
  j.add(list);
  j.add(jt);
  j.add(tp);

  BAdd.setBounds(5,20, 90,30);
  BAdd.setBackground(new Color(255,255,255));
  BDelect.setBounds(5, 80, 90, 30);
  BDelect.setBackground(new Color(255,255,255));
  BDelectTable.setBounds(5, 140, 90, 30);
  BDelectTable.setBackground(new Color(255,255,255));
  TablePlaer.setBounds(30, 100, 200, 50);
  TablePlaer.setFont(new Font("宋体",1, 20));

  p1.add(TablePlaer);
  BMovePrevious.setBackground(new Color(255,255,255));
  BPlayer.setBackground(new Color(255,255,255));
  BMoveNext.setBackground(new Color(255,255,255));
  BStop.setBackground(new Color(255,255,255));
  select.setBackground(new Color(255,255,255));
  BSet.setBackground(new Color(255,255,255));
  p2.add(BMovePrevious);
  p2.add(BPlayer);
  p2.add(BMoveNext);
  p2.add(BStop);
  p2.add(select);
  p2.add(BSet);
  p2.setBackground(new Color(255,255,255));

  p.add(p2,BorderLayout.WEST);
  p.add(p3,BorderLayout.CENTER);

  p5.add(p);
  p5.add(BAdd);
  p5.add(BDelect);
  p5.add(BDelectTable);

  BAdd.addActionListener(this);
  BDelect.addActionListener(this);
  BDelectTable.addActionListener(this);

  BMoveNext.addActionListener(this);
  BPlayer.addActionListener(this);
  BMovePrevious.addActionListener(this);
  BStop.addActionListener(this);
  select.addActionListener(this);
  BSet.addActionListener(this);
  timeInformation.setEnabled(false);
   /*
   * 实现进度条
   * */ 

   timeSlider.setMajorTickSpacing(1);//调用此方法设置主刻度标记的间隔。传入的数字表示在每个主刻度标记之间以值衡量的距离。
   timeSlider.setPaintTicks(true); //要绘制主刻度,setPaintTicks 必须设置为 true
   timeSlider.addChangeListener(new ChangeListener() { //创建一个新的 ChangeListener 添加到滑块。
    public void stateChanged(ChangeEvent arg0) {
     if (player != null && ispressing == 1) {
      newtime = (int)((JSlider)arg0.getSource()).getValue();
      timeInformation.setText("当前时间:"+newtime/60+":"+newtime%60+"  ||  "+" 总时间: "+countSecond/60+":"+countSecond%60);
      ischanging = 1;
     }
    }
   });
   timeSlider.addMouseListener(new MouseAdapter(){
    public void mousePressed(MouseEvent arg0) {
     ispressing = 1;   //当鼠标对游标进行点击时
    }
    public void mouseReleased(MouseEvent arg0) {
     ispressing = 0;   //当鼠标不对游标进行点击时
    }
   });
   timeInformation.setText("当前时间:00:00  ||  总时间:00:00");
   timeInformation.setBackground(new Color(255,255,255));
   p3.add(timeInformation,BorderLayout.NORTH);
   p3.add(timeSlider,BorderLayout.SOUTH);

   j.add(pp);
   j.add(p5);
   j.add(p);
   j.add(p1);
   j.add(p6);
   j.setVisible(true);
//  j.setResizable(false);
 }

 /*
  * 主函数
  * */

 public static void main(String[] args) throws IOException, InterruptedException { //InterruptedException:当线程在活动之前或活动期间处于正在等待、休眠或占用状态且该线程被中断时,抛出该异常
  MyMusicPlayer play=new MyMusicPlayer();
  Thread timeRun = new Thread(play);
  timeRun.start();
 }
 @Override
 public void actionPerformed(ActionEvent e) {
  String cmd = e.getActionCommand();     //通过获取字符串来判断是播放还是暂停,
  String box=(String)select.getSelectedItem();   //判断播放的顺序
  if(e.getSource()==BAdd)
  {
   if (player == null) {
    if (fileChooser.showOpenDialog(j) == MusicFileChooser.APPROVE_OPTION) {
     this.MusicName = fileChooser.getSelectedFile();
     File cd = fileChooser.getCurrentDirectory(); //获取当前路径
     if (cd != this.currentDirectory|| this.currentDirectory == null) {
      FileFilter[] fileFilters = fileChooser.getChoosableFileFilters(); //FileFilter 是一个抽象类,JFileChooser 使用它过滤显示给用户的文件集合
      File files[] = cd.listFiles(); //cd.listFiles()表示返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。
      this.MusicNames = new ArrayList<File>();
      for (File file : files) { //每次循环都将数组中的文件对象赋给file这个变量,然后再在循环体中对这个变量进行操作如:
             //for(int i=0;i<files.length;i++){ file = files[i];……}
       filename = file.getName().toLowerCase();   //获取所有的音乐名称
       for (FileFilter filter : fileFilters) {
        if (!file.isDirectory() && filter.accept(file)) {
         this.MusicNames.add(file);
          list.add(filename);
         filename=e.getActionCommand();
        }
       }
      }
     }
     index = MusicNames.indexOf(MusicName); //定义歌曲的下标
     count = MusicNames.size();
     PlayFile();
    }
   } else {
    player.start();
   }

  }

  if(cmd.equals("暂停")){
   BPlayer.setText("播放");
   player.stop();
   }
  if(cmd.equals("播放")){
   BPlayer.setText("暂停");
   player.start();
  }

  if(e.getSource()==BStop){   //停止
    if (player != null) {

     player.stop();
     timeInformation.setText("当前时间:00:00  ||  总时间:00:00");
     timeSlider.setValue(0);
     player.setMediaTime(new Time(0)); //设置时间为零
  }
    }
  if(e.getSource()==BMoveNext){  //下一曲
    if (player != null) {
     if("顺序播放".equals(box)){
      nextMusic();
     }
     if("随机播放".equals(box)){
      int index = (int) rand.nextInt(this.MusicNames.size())+1;
       if (this.MusicNames != null && !this.MusicNames.isEmpty()) {
         if ( ++index == this.MusicNames.size()) {
          index=(int) rand.nextInt(this.MusicNames.size())+1;
         }
         player.stop();   //若点击上一曲,则将当前的歌曲停止播放,播放上一曲
         try {
          player=Manager.createRealizedPlayer(MusicNames.get(index).toURI().toURL());
          player.prefetch();
          player.setMediaTime(new Time(0.0));// 从某个时间段开始播放
          player.addControllerListener(this);
          l.setText("正在播放:"+this.MusicNames.get(index).toString());
          list.select(index);
          player.start();
          flag=1;
         } catch (NoPlayerException | CannotRealizeException | IOException e1) {
          e1.printStackTrace();
         }
       }
     }
    }
  }
  if(e.getSource()==BMovePrevious){  //上一曲
    if (player != null) {
     if("顺序播放".equals(box)){
      PreviousMusic();
     }
     if("随机播放".equals(box)){
      int index = (int) rand.nextInt(this.MusicNames.size())+1;
      if (this.MusicNames != null && !this.MusicNames.isEmpty()) {
       if ( index--==(int) rand.nextInt(this.MusicNames.size())+1) {
        index = this.MusicNames.size() - 1;
       }
       player.stop();    //若点击上一曲,则将当前的歌曲停止播放,播放上一曲
       try {
        player=Manager.createRealizedPlayer(MusicNames.get(index).toURI().toURL());
        player.prefetch();
        player.setMediaTime(new Time(0.0));// 从某个时间段开始播放
        player.addControllerListener(this);
        l.setText("正在播放:"+this.MusicNames.get(index).toString());
        list.select(index);
        player.start();
        flag=1;
       } catch (NoPlayerException | CannotRealizeException | IOException e1) {
        e1.printStackTrace();
       }
      }
    }
    }
  }
  if(e.getSource()==BDelect){    //删除歌曲
   index =list.getSelectedIndex();
   list.remove(index);
   MusicNames.remove(this.index);
  }
  if(e.getSource()==BDelectTable){   //清空列表

   list.removeAll();
   MusicNames.removeAll(MusicNames);
   player.stop();
   player = null;
  }

  //双击列表时实现播放
  list.addMouseListener(new MouseAdapter() {
   public void mouseClicked(MouseEvent e) {
    // 双击时处理
    if (e.getClickCount() == 2) {
     if(player!=null){
      player.stop();
     }
     // 播放选中的文件
     index=list.getSelectedIndex();
     PlayFile();
    }
   }
  });

}

 // 因为实现了"ControllerListener"接口,本方法用于处理媒体播放器传来的事件;
 public void controllerUpdate(ControllerEvent e) {
  String box=(String)select.getSelectedItem();   //判断播放的顺序
  if (e instanceof EndOfMediaEvent) {
   player.setMediaTime(new Time(0));
   if ("单曲循环".equals(box)) {
    player.start();
   }
   if("顺序播放".equals(box)){
    nextMusic();
   }
   if("随机播放".equals(box)){
     if (this.MusicNames != null && !this.MusicNames.isEmpty()) {
       int index = (int) rand.nextInt(this.MusicNames.size())+1;
       try {
        player=Manager.createRealizedPlayer(MusicNames.get(index).toURI().toURL());
        player.prefetch();
        player.setMediaTime(new Time(0.0));// 从某个时间段开始播放
        player.addControllerListener(this);
        l.setText("正在播放:"+this.MusicNames.get(index).toString());
        list.select(index);
        player.start();
        flag=1;
       } catch (NoPlayerException | CannotRealizeException | IOException e1) {
        e1.printStackTrace();
       }
     }
   }
  }
 }

  /**
  * 获取MP3歌曲名
  *
  * @MP3文件路径
  * @歌曲名
  */
 public String getMusicName(String str) {
  int i;
  for (i = str.length() - 1; i > 0; i--) {
   if (str.charAt(i) == '\\')
    break;
  }
  str = str.substring(i + 1, str.length() - 4);
  return str;
 }

 /**
  * 下一首 实现函数
  */
 public void nextMusic() {
 }

 /**
  * 上一首实现函数
  */
 public void PreviousMusic() {
 }

 /**
  * 播放MP3文件主函数
  */
 public void PlayFile() {
  try {
   player = Manager.createRealizedPlayer(MusicNames.get(index).toURI().toURL());
   player.prefetch();
   player.setMediaTime(new Time(0.0));// 从某个时间段开始播放
   player.addControllerListener(this);
   l.setFont(new Font("宋体",0,20));
   l.setText("正在播放:"+this.MusicNames.get(index).toString()); //显示正在播放的歌曲
   list.select(index);
   player.start();

   Mythread11 tt=new Mythread11();
   tt.start();

  } catch (Exception e1) { //当选到一首音乐不能播放时,对其进行处理
   dealError();
   return;
  }
  this.setFrame();
  }

 public void setFrame()
 {
  countSecond = (int)player.getDuration().getSeconds();
  timeSlider.setMaximum(countSecond);
  timeSlider.setValue(0);
  newtime = 0;
 }

private void dealError() {
  // TODO Auto-generated method stub
 MusicNames.remove(index);
 if( --count == index )
  index = 0;
 if( count == 0)
  player = null;
 else
  PlayFile();
 }

/**
 * MP3文件筛选内部类
 */
class MusicFileChooser extends JFileChooser {
 }

/**
 * MP3文件筛选辅助内部类
 *
 */
class MyFileFilter extends FileFilter { //FileFilter 是一个抽象类,JFileChooser 使用它过滤显示给用户的文件集合
 String[] suffarr;
 String decription;

 public MyFileFilter() {
  super();
 }

 public MyFileFilter(String[] suffarr, String decription) {
  super();
  this.suffarr = suffarr;
  this.decription = decription;
 }

 public boolean accept(File f) {
  for (String s : suffarr) {
   if (f.getName().toUpperCase().endsWith(s)) {
    return true;
   }
  }
  return f.isDirectory();
 }

 public String getDescription() {
  return this.decription;
 }
}

/**
 * 读取显示时间进度条
 */
public void run() {
 while(true) {
  sleep();
  if(player != null) {
   if(ispressing == 0) {
    if(ischanging == 1) {
     newtime = timeSlider.getValue();
     player.setMediaTime(new Time(((long)newtime)*1000000000));
     ischanging = 0;
    } else {
     newtime = (int)player.getMediaTime().getSeconds();
     timeSlider.setValue(newtime);
     timeInformation.setText("当前时间:"+newtime/60+":"+newtime%60+"  ||  "+" 总时间: "+countSecond/60+":"+countSecond%60);

    }

   }
  }
 }
}

//实现歌词的线程
class Mythread11 extends Thread {
 public void run() {
  // TODO Auto-generated method stub
  try{
   LRC lrc = ReadLRC.readLRC("Traveling Light.lrc");
   Lyrics ls = ParseLRC.parseLRC(lrc);
   playTest(ls);
  }catch(Exception e){

  }
 }

}
static void playTest(Lyrics ls) throws InterruptedException {
 tp.setFont(new Font("宋体",1,20));
 tp.setForeground(Color.BLUE);
 StyledDocument doc = tp.getStyledDocument();
 SimpleAttributeSet center = new SimpleAttributeSet();
 StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);  //将歌词区中显示
 doc.setParagraphAttributes(0, doc.getLength(), center, false);
 tp.setText("艺术家:" + ls.getAr());
 tp.setText("专辑:" + ls.getAl());
 tp.setText("歌曲:" + ls.getTi());
 tp.setText("歌词制作:" + ls.getBy());
 for (Lyric l : ls.getLyrics()) {
  tp.setText( l.getTxt());
  Thread.sleep(l.getTimeSize());
 }
}

}

五、总的测试效果

如下

更多关于播放器的内容请点击《java播放器功能》进行学习。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Java swing仿酷狗音乐播放器

    今天给大家介绍下用Java swing开发一款音乐播放器,高仿酷狗音乐播放器,完整源码地址在最下方,本文只列出部分源码,因为源码很多,全部贴不下,下面还是老规矩.来看看运行结果: 下面我们来看看代码: 首先看一下主窗口的实现代码: package com.baiting; import java.awt.Dimension; import java.awt.Toolkit; import com.baiting.menu.CloseWindow; /** * 窗口 * @author lmq *

  • 简单实现java音乐播放器

    学习过java语言的你,或多或少,在某天突发奇想,想着用swing做一个音乐播放器.但是,发现很难找到,相关的java代码,或者你下载的代码有问题,或者你代码里面引入的类包找不到.为了解决自如此类的问题.在这儿,有如下的代码可以供大家参考. package TheMusic; import java.io.*; import javax.sound.sampled.*; public class Music { public static void main(String[] args) { /

  • java音乐播放器实现代码

    本文实例为大家分享了java音乐播放器的具体代码,供大家参考,具体内容如下 这个是源码结构介绍 这个是界面,有点简陋,见笑了,但是基本上的东西都有了,没办法,没有美工的程序写的界面 直接上源代码Player.java package com.service; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Random; import javax.sound

  • java 实现音乐播放器的简单实例

    java 实现音乐播放器的简单实例 实现效果图: 代码如下 package cn.hncu.games; import java.applet.Applet; import java.applet.AudioClip; import java.awt.Color; import java.awt.Font; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.Mou

  • java音乐播放器编写源码

    本文实例为大家分享了java音乐播放器的具体代码,供大家参考,具体内容如下 源码: package baidu; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; import javax.swing.*; import javax.media.bean.playerbean.*; //这个包要用到JMF public class MP3 extends JFrame impleme

  • 一个简单的Java音乐播放器

    本文实例为大家分享了Java音乐播放器展示的具体代码,供大家参考,具体内容如下 package KKMusic; import java.applet.Applet; import java.applet.AudioClip; import java.awt.BorderLayout; import java.awt.EventQueue; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.borde

  • 利用java制作简单的音乐播放器

    本文主要是用到java中的swing技术,以及JMFjar中的API,为大家分享了java音乐播放器的具体实现代码,供大家参考,具体内容如下 备注:需要用JDK1.8才能播放音乐MP3 package baidu; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; import javax.swing.*; import javax.media.bean.playerbean.*;

  • java音乐播放器课程设计

    一.课程设计目的 1.编程设计音乐播放软件,使之实现音乐播放的功能. 2.培养学生用程序解决实际问题的能力和兴趣. 3.加深java中对多媒体编程的应用. 二.课程设计的要求 利用学到的编程知识和编程技巧,要求学生: 1.系统设计要能完成题目所要求的功能,设计的软件可以进行简单的播放及其他基本功能. 2.编程简练,可用,尽可能的使系统的功能更加完善和全面 3.说明书.流程图要清楚. 三.课程设计内容 1.课程设计的题目及简介 音乐播放软件要求: 有图形界面,能播放MP3歌曲,有播放列表,前一首.

  • 教你轻松制作java音乐播放器

    一.音乐播放器的实现原理 Javase的多媒体功能很弱,所以有一个专门处理多媒体的插件叫JMF,JMF提供的模型可大致分为七类 * 数据源(Data source) * 截取设备(Capture Device,包括视频和音频截取设备) * 播放器(Player) * 处理器(Processor) * 数据池(DataSink) * 数据格式(Format) * 管理器(Manager) 而我所做的这个音乐播放器MyMusicPlayer(这是我创建的类名)正是调用了JMF中的Player类来实现

  • 教你轻松制作Android音乐播放器

    欣赏一下我们清爽的界面吧~ 如果是只用activity来制作这样的东西简直是太小儿科了,此处我们当然用的是service 首先我们先上service的代码: 1.如果我们要访问service的属性和方法,那么在activity肯定是以bindservice的方法实现的,而在service中的onbind方法也是必须要实现的,onbind返回的Ibinder对象在activity的serviceconnection中得到使用. 2.activity获取到Ibinder对象,可以进一步获取服务对象和

  • 运用js教你轻松制作html音乐播放器

    用HTML做了个音乐播放器,可以循环播放,选择歌曲,以及自动播放下一首,运用了js和json知识,下面是效果图和源码,有兴趣的可以试试哦 效果图: 源码:html <span style="color:#999999;"><!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>音乐播放器</title> <sc

  • JS+html5制作简单音乐播放器

    本教程为大家分享了JS音乐播放器的具体代码,供大家参考,具体内容如下 1.HTML <audio> 标签定义声音,比如音乐或其他音频流.其主要属性有src:要播放的音频的 URL,controls:如果出现该属性,则向用户显示控件,比如播放按钮. 几个主要的标签如下: <div> <h4 id="name">李玉刚 - 刚好遇见你</h4> <br> <audio id="audio" src=&qu

  • 教你轻松制作java视频播放器

    前言 跳过废话,直接看正文 当年入坑Java是因为它的跨平台优势.那时我认为,"编写一次,处处运行."这听上去多么牛逼,应该是所有语言发展的终极之道,java势必会一统天下. 然而事实证明,那时的我还是太年轻. 正所谓鱼和熊掌不可兼得,若要享受跨平台带来的方便,便不可避免地要接受性能上的不足.事实上,java一直在致力于提高虚拟机的性能(JIT等技术),但面对对实时计算性能要求很高或涉及到用硬件优化的任务(视频的硬件编码.解码)时,仍远远比不上c或c++.因此,很少能够看到有人用jav

随机推荐