구글에서 Android Audio Player code 를 입력하여 검색하면 가장 먼저 검색되는 것이 https://www.androidhive.info/2012/03/android-building-audio-player-tutorial/ 게시글이다.
구글 계정을 허용하고 파일을 다운로드하면 Eclipse 기반 소스코드가 다운로드된다.
이 코드를 실행하면 바로 에러가 발생하면서 어플이 죽어버린다.
오디오 플레이 코드 구현을 통해서 기능을 익히고자 검색하고 몇가지 어플 코드를 참조하여 만들어보고 있는 중이다.
현재까지 구현한 APK 파일
linkaudioplayer-release.apk
설계 관점
1. 스마트폰에 있는 모든 음악파일을 검색해서 Custom ListView 에 보여준다.
RecyclerView 가 ListView 의 단점을 개선한 ListView 인데 이 글 마지막 부분에 링크된 걸 참조하라.
2. Custom ListView 에 곡 정보를 클릭하면 AudioPlayer가 바로 재생된다.
곡을 길게 클릭하면 전체선택, 선택한 리스트만 AudioPlayer ArrayList 에 담아서
순차재생, 특정곡 반복재생, 랜덤재생을 할 수 있도록 한다.
3. 재생, 잠시멈춤, 5초 앞으로, 5초 뒤로, 이전곡, 다음곡 버튼을 제공한다.
위 링크글에 구현되어 있으니 활용하면 된다.
4. 전화를 받으면 자동으로 재생되던 음악이 멈추고 통화가 끝나면 다시 음악이 재생된다.
1. 스마트폰에 있는 음악파일 리스트 가져오기
private void loadAudioList() { ContentResolver contentResolver = getContentResolver(); // 음악 앱의 데이터베이스에 접근해서 mp3 정보들을 가져온다.
Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; String selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0"; String sortOrder = MediaStore.Audio.Media.TITLE + " ASC"; Cursor cursor = contentResolver.query(uri, null, selection, null, sortOrder); cursor.moveToFirst(); System.out.println("음악파일 개수 = " + cursor.getCount()); if (cursor != null && cursor.getCount() > 0) { do { long track_id = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media._ID)); long albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID)); String title = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE)); String album = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM)); String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST)); Integer mDuration = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION)); String datapath = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)); System.out.println("mId = " + track_id + " albumId = " + albumId + " title : " + title + " album : " + album + " artist: " + artist + " 총시간 : " + mDuration + " data : " + data); // Save to audioList listViewAdapter.addItem(track_id, albumId, title, artist, album, mDuration, datapath, false); } while (cursor.moveToNext()); } cursor.close(); } |
ContentResolver 를 통해서 음악파일을 가져온다.
가져온 음악파일을 ArrayList 에 추가한다.
Song_Item 에 checkBoxState boolean 을 넣은 이유는 선택한 곡만 true, false 하기 위해서다.
import java.io.Serializable;
public class Song_Item implements Serializable { private long mId; // 오디오 고유 ID private long AlbumId; // 오디오 앨범아트 ID private String Title; // 타이틀 정보 private String Artist; // 아티스트 정보 private String Album; // 앨범 정보 private Integer Duration; // 재생시간 private String DataPath; // 실제 데이터 위치 boolean checkBoxState;
public Song_Item() { }
public Song_Item(long mId, long albumId, String title, String artist, String album, Integer duration, String dataPath, boolean checkBoxState) { this.mId = mId; AlbumId = albumId; Title = title; Artist = artist; Album = album; Duration = duration; DataPath = dataPath; this.checkBoxState = checkBoxState; }
public long getmId() { return mId; }
public void setmId(long mId) { this.mId = mId; }
public long getAlbumId() { return AlbumId; }
public void setAlbumId(long albumId) { AlbumId = albumId; }
public String getTitle() { return Title; }
public void setTitle(String title) { Title = title; }
public String getArtist() { return Artist; }
public void setArtist(String artist) { Artist = artist; }
public String getAlbum() { return Album; }
public void setAlbum(String album) { Album = album; }
public Integer getDuration() { return Duration; }
public void setDuration(Integer duration) { Duration = duration; }
public String getDataPath() { return DataPath; }
public void setDataPath(String dataPath) { DataPath = dataPath; }
public boolean isCheckBoxState() { return checkBoxState; }
public void setCheckBoxState(boolean checkBoxState) { this.checkBoxState = checkBoxState; } } |
listViewAdapter.addItem(track_id, albumId, title, artist, album, mDuration, datapath, false);
// 음악 데이터 추가를 위한 메소드 public void addItem(long mId, long AlbumId, String Title, String Artist, String Album, Integer Duration, String DataPath, boolean checkItem_flag) { Song_Item item = new Song_Item(); item.setmId(mId); item.setAlbumId(AlbumId); item.setTitle(Title); item.setArtist(Artist); item.setAlbum(Album); item.setDuration(Duration); item.setDataPath(DataPath); item.setCheckBoxState(checkItem_flag); songsList.add(item); }
|
private ListView audiolistView; // 리스트뷰
private ArrayList<Song_Item> songsList = null; // 음악 전체 데이터 리스트
private ArrayList<Song_Item> playList = new ArrayList<>(); // 선택한 음악 데이터 리스트
private ListViewAdapter listViewAdapter = null; // 리스트뷰에 사용되는 ListViewAdapter
전체 선택, 선택 해제 버튼 구현 코드
// all checkbox checkAll = (CheckBox) findViewById(R.id.lv_checkbox_all); checkAll.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { if (checkAll.isChecked() == true) { isAll = true; listViewAdapter.setAllChecked(checkAll.isChecked()); listViewAdapter.notifyDataSetChanged(); } else { isAll = false; listViewAdapter.setAllChecked(checkAll.isChecked()); listViewAdapter.notifyDataSetChanged(); } } });
final Button calcel = (Button) findViewById(R.id.btn_cancle); calcel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { isMSG = false; relative1.setVisibility(View.GONE); listViewAdapter.setAllChecked(false); checkAll.setChecked(false); // 전체 선택 체크박스 해제 listViewAdapter.notifyDataSetChanged(); } }); |
Button send = (Button) findViewById(R.id.btn_send); // 선택한 곡 Player.class 로 전송 send.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { playList.clear(); for(int i = 0; i< songsList.size(); i++){ if(songsList.get(i).isCheckBoxState() == true){ PlayList(songsList.get(i).getmId(), songsList.get(i).getAlbumId(), songsList.get(i).getTitle(), songsList.get(i).getArtist(), songsList.get(i).getAlbum(), songsList.get(i).getDuration(), songsList.get(i).getDataPath()); } }
if(playList.size() ==0){ Toast.makeText(MainActivity.this, "선택한 리스트가 없습니다.", Toast.LENGTH_SHORT).show(); return; } else { Intent intent = new Intent(MainActivity.this, Player.class); intent.putExtra("playList", playList); // 배열 데이터 startActivity(intent);
calcel.performClick(); // 버튼 강제로 자동 실행 } } });
// 음악 재생 데이터 추가를 위한 메소드 public void PlayList(long mId, long AlbumId, String Title, String Artist, String Album, Integer Duration, String DataPath) { Song_Item item = new Song_Item(); item.setmId(mId); item.setAlbumId(AlbumId); item.setTitle(Title); item.setArtist(Artist); item.setAlbum(Album); item.setDuration(Duration); item.setDataPath(DataPath); playList.add(item); }
|
Player.Class 주요 코드
// 배열로 넘긴 데이터를 배열로 받는 방법
private ArrayList<Song_Item> list;
Intent intent = getIntent();
list =(ArrayList<Song_Item>) intent.getSerializableExtra("playList");
private int position = 0; // 현재 재생곡 위치
playMusic(list.get(position)); // 노래 곡
public void playMusic(Song_Item song_item) {
try {
songTitleLabel.setText(song_item.getTitle());
mediaPlayer.reset();
mediaPlayer.setDataSource(song_item.getDataPath());
mediaPlayer.prepare();
mediaPlayer.start(); // 노래 재생 시작
if(mediaPlayer.isPlaying()){
btnPlay.setVisibility(View.GONE);
btnPause.setVisibility(View.VISIBLE);
} else {
btnPlay.setVisibility(View.VISIBLE);
btnPause.setVisibility(View.GONE);
}
/* Album Art Bitmap을 얻는다. */
final Uri artworkUri = Uri.parse("content://media/external/audio/albumart");
ImageView mImgAlbumArt = (ImageView) findViewById(R.id.albumart);
Uri albumArtUri = ContentUris.withAppendedId(artworkUri, song_item.getAlbumId());
Picasso.with(Player.this)
.load(albumArtUri)
.resize(800,800)
.into(mImgAlbumArt);
// set Progress bar values
songProgressBar.setProgress(0);
songProgressBar.setMax(100);
// Updating progress bar
updateProgressBar();
} catch (IOException e) {
e.getMessage();
Toast.makeText(Player.this, "Error!!", Toast.LENGTH_SHORT).show();
}
}
Albumart 이미지를 얻는 방법
1 |
ImageView mImgAlbumArt = (ImageView) findViewById(R.id.albumart); Bitmap albumArt = getArtworkQuick(Player.this, (int) song_item.getAlbumId(), 800, 800); mImgAlbumArt.setImageBitmap(albumArt); |
2 |
final Uri artworkUri = Uri.parse("content://media/external/audio/albumart"); ImageView mImgAlbumArt = (ImageView) findViewById(R.id.albumart); Uri albumArtUri = ContentUris.withAppendedId(artworkUri, song_item.getAlbumId()); Picasso.with(Player.this).load(albumArtUri).into(mImgAlbumArt); |
검색을 하면 getArtworkQuick 메소드를 이용해서 하는 방법이 검색될 것이다.
이걸로 컴파일한 것이 폰이 낮은 버전에서 제대로 동작이 안되는 증상(?)이 있었다.
2번째 방법으로 하면 쉽게 해결된다.
Picasso(피카소)는 안드로이드를 위한 이미지 라이브러리다.
이것은 Square에 의해 만들어졌고, 이미지를 불러오거나 처리할 수 있도록 해준다.
피카소는 이미지를 디스플레이하는 과정을 외부 장소를 통해 단순화 시킨다.
이 유용한 라이브러리를 이용하면 고작 몇 줄로 이미지를 로딩할 수 있다.
build.gradle 파일 안에 dependency 코드 안에 compile 'com.squareup.picasso:picasso:2.5.2' 을 추가하면 자동 설치된다.
SeekBar 에 대한 것은 구글링을 해서 코드를 좀 찾아서 수정하거나
https://www.androidhive.info/2012/03/android-building-audio-player-tutorial/ 에 첨부파일 참조해서 구현하면 된다.
public void updateProgressBar() { seekHandler.postDelayed(mUpdateTimeTask, 100); }
private Runnable mUpdateTimeTask = new Runnable() { public void run() { if(mediaPlayer != null){ long totalDuration = mediaPlayer.getDuration(); long currentDuration = mediaPlayer.getCurrentPosition();
// 총 재생시간 화면 표시 songTotalDurationLabel.setText("" + UtilitiesHelper.milliSecondsToTimer(totalDuration)); // 현 재생시간 화면 표시 songCurrentDurationLabel.setText("" + UtilitiesHelper.milliSecondsToTimer(currentDuration));
// Updating progress bar int progress = (int)(UtilitiesHelper.getProgressPercentage(currentDuration, totalDuration)); songProgressBar.setProgress(progress);
// Running this thread after 100 milliseconds seekHandler.postDelayed(this, 100); } } };
SeekBar.OnSeekBarChangeListener seekbarListener = new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { try { if(mediaPlayer.isPlaying() || mediaPlayer != null){ if(fromUser) mediaPlayer.seekTo(progress); } else if(mediaPlayer == null){ Toast.makeText(getApplicationContext(), "Media is not running", Toast.LENGTH_SHORT).show(); seekBar.setProgress(0); } if (seekBar.getMax()==progress) { songProgressBar.setProgress(0); long currentDuration = 0; songCurrentDurationLabel.setText("" + UtilitiesHelper.milliSecondsToTimer(currentDuration)); } } catch (Exception e){ Log.e("seek bar", "" + e); seekBar.setEnabled(false); }
}
@Override public void onStartTrackingTouch(SeekBar seekBar) { mediaPlayer.pause(); // 손으로 트랙바를 움직이는 동안 재생 중지 seekHandler.removeCallbacks(mUpdateTimeTask); }
@Override public void onStopTrackingTouch(SeekBar seekBar) { seekHandler.removeCallbacks(mUpdateTimeTask); int totalDuration = mediaPlayer.getDuration(); int currentPosition = UtilitiesHelper.progressToTimer(seekBar.getProgress(), totalDuration);
// 손으로 움직인 지점, forward, backward 지점부터 재생 mediaPlayer.seekTo(currentPosition); if(seekBar.getProgress()>0 ){ mediaPlayer.start(); } // update timer progress again updateProgressBar(); } }; |
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { seekHandler.removeCallbacks(mUpdateTimeTask); if(isRepeat){ // 반복재생 playMusic(list.get(position)); } else if(isShuffle){ // 랜덤재생 Random rand = new Random(); position = rand.nextInt((list.size() - 1) - 0 + 1) + 0; playMusic(list.get(position)); } else { if(position + 1 < list.size()) { position++; playMusic(list.get(position)); } else { Log.e("Music_Off","음악종료"); position = 0; getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); acquireCPUWakeLock(mContext); releaseCpuLock(); } }
} });
|
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.tistory.link2me.audioplayer">
<permission android:name="android.permission.MEDIA_CONTENT_CONTROL" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" />
<application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" android:noHistory="true"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".Player"></activity> </application>
</manifest> |
ListView 방식을 RecyclerView 방식으로 변경해보면서 변경할 부분에 중점적으로 적어둔 것은 http://link2me.tistory.com/1374 에 있다.
앞으로 분석하면 도움이 될 사항이라 링크를 적어둔다.https://github.com/googlesamples/android-UniversalMusicPlayer