📜 ⬆️ ⬇️

Creating a home audio system

Immediately make a reservation that I understand by the home audio system.

Objective: To control the playback of music in columns from any device on your home network.

The implementation looks like this: the database stores information about the music, there is a servlet that pulls this information upon request.
Everything is managed through a web interface.
And, finally, on the android device to which the speakers are connected, the audio server is spinning.
')
And so, let's go.

1. Prepare the database


We will use MySQL as a database. The database contains two tables: mp3 - data about audio files and mp3_tmp - the table is used when updating the database. The structure of both tables is identical.

The tables contain the following fields:



path - the path to the file on disk, PRIMARY KEY ;
artist - performer;
album - the name of the album;
title - the name of the track;
year - year of recording;
number - the number of the track in the album;
length - the length of the track in the format mm: ss.

So, SQL to create the table:
DROP TABLE IF EXISTS `mp3_tmp`; CREATE TABLE `mp3_tmp` ( `path` varchar(250) NOT NULL, `artist` varchar(250) DEFAULT NULL, `album` varchar(250) DEFAULT NULL, `title` varchar(250) DEFAULT NULL, `year` varchar(40) DEFAULT NULL, `length` varchar(40) DEFAULT NULL, `number` varchar(40) DEFAULT NULL, PRIMARY KEY (`path`)) ENGINE=InnoDB DEFAULT CHARSET=utf8; 


For the formation of the table using the program in Java.

To begin with, let's create a list of all mp3 files in the folder with music:

 FileFinder ff = new FileFinder(); //   ,    List<File> files = ff.findFiles(initPath, ".*\\.mp3"); //   mp3-   initPath 


Next, we enter the data about the files in the temporary table.
(used JDBC - to connect to the database and
jaudiotagger library - to scan mp3 tags):

 for (File file : files) { //  : PreparedStatement preparedStatement = connect.prepareStatement("insert into `mp3_tmp` " + "(path, artist, album, title, year, number, length) " + "values (?, ?, ?, ?, ?, ?, ?)"); String fullName = file.getCanonicalPath(); String fileName = fullName.replace(initPath,""); //       String length = ""; try { AudioFile af = AudioFileIO.read(file); int len = af.getAudioHeader().getTrackLength(); //   int sec = len%60; int min = (len-sec)/60; length = String.format("%02d:%02d", min, sec); //  MP3File mp3f = new MP3File(file); Tag tag = mp3f.getTag(); //  String artist = tag.getFirst(FieldKey.ARTIST); String album = tag.getFirst(FieldKey.ALBUM); String title = tag.getFirst(FieldKey.TITLE); String year = tag.getFirst(FieldKey.YEAR); String number = tag.getFirst(FieldKey.TRACK); //      (  ): if (!number.equals("")){ Integer num = Integer.parseInt(number); if (num < 10) { number = "00"+num.toString(); } else if (num < 100) { number = "0"+num.toString(); } } //   : preparedStatement.setString(1, fileName); preparedStatement.setString(2, artist); preparedStatement.setString(3, album); preparedStatement.setString(4, title); preparedStatement.setString(5, year); preparedStatement.setString(6, number); } catch (Exception e) { //    ,   : preparedStatement.setString(1, fileName); preparedStatement.setString(2, ""); preparedStatement.setString(3, ""); preparedStatement.setString(4, ""); preparedStatement.setString(5, ""); preparedStatement.setString(6, ""); } finally { preparedStatement.setString(7, length); preparedStatement.executeUpdate(); //     } } //, ,   : statement.execute("DROP TABLE IF EXISTS `mp3`"); statement.execute("CREATE TABLE `mp3` LIKE `mp3_tmp`"); statement.execute("INSERT INTO `mp3` SELECT * FROM `mp3_tmp`"); 


2. Backend player


The player's backend consists of two servlets:

@WebServlet ("/ getlist") - returns the list of tracks for the search query;
@WebServlet ("/ hint") - returns hints on the first letters.

Learn more about the GetList servlet.

 // SQL- String query = "select path, artist, title, album, year, length from `mp3` where "; String[] qArr0 = request.getParameter("query").split("\\|"); for (int k=0; k<qArr0.length; k++) { if (k>0) query += " or "; String[] qArr = qArr0[k].split(" "); query += "concat(title,' ',album,' ',artist) like "+"'%"+qArr[0]+"%' "; for (int j=1; j<qArr.length; j++) { query += "and concat(title,' ',album,' ',artist) like "+"'%"+qArr[j]+"%' "; } } query += "group by concat(year,album,number,title,artist,length) "; query += "order by concat(year,' ',album,' ',number,' ',title,' ',artist)"; Statement statement = connect.createStatement(); resultSet = statement.executeQuery(query); //  //  GSON   json Gson gson = new GsonBuilder().create(); int i=0; while (resultSet.next()) { if (i++>0) playlist+="\n"; String artist = resultSet.getString("artist"); String title = resultSet.getString("title"); String album = resultSet.getString("album"); String year = resultSet.getString("year"); String path = resultSet.getString("path"); path = musicPath+path; String length = resultSet.getString("length"); Track track = new Track(title, artist, album, year, path, length); playlist += gson.toJson(track); } 


The Hint servlet runs the following SQL query:

 // String text = request.getParameter("query"); Statement statement = connect.createStatement(); resultSet = statement.executeQuery("select title as 'str' from " + "`mp3` where title like '%" + text + "%' union " + "select artist as 'str' from " + "`mp3` where artist like '%" + text + "%' union " + "select album as 'str' from " + "`mp3` where album like '%" + text + "%' group by str order by str limit 10"); 


and returns the data in the json format.

3. HTML5 frontend


We use HTML5 and jQuery as a front end player. The following points are interesting here.

Playlist Formation:

 $('#search').click(function(){ // -   var uri = '/mp3player/getlist?query='+$('#query').val(); $.get(encodeURI(uri),function(data){ window.playlist = []; window.number = 0; var array = data.split('\n'); for (i=0; i<array.length; i++) { window.playlist[i] = $.parseJSON(array[i]); } //  HTML : var li_list = '<ol>'; var album = ''; var year = ''; var alb_num = 0; for (i=0; i<window.playlist.length; i++) { if (album != window.playlist[i].album || i == 0) { album = window.playlist[i].album; year = window.playlist[i].year; var str = ' '; if (album) str = album; if (year) str+= ' | ' + year; li_list += '<h4 class="album" alb_num='+alb_num+'>'+str+'</h4>'; alb_num++; } li_list += '<li class="track" id='+i+' alb_num='+alb_num+'>' +window.playlist[i].title+' ('+window.playlist[i].artist+') | '+window.playlist[i].length+'</li>'; } li_list += '</ol>'; //  : $('#list').html(li_list); if (window.playlist.length>1) { window.alb_num = $('li#'+window.number).attr('alb_num'); $('#list').scrollTop($('li#'+window.number).offset().top-$('li#0').offset().top); } //      : $('li.track').click(function(){ $('#toogle').prop('disabled',false); $('#toogle').prop('checked',true); $('li#'+window.number).attr('style','font-weight:normal; font-style:normal'); window.number = $(this).attr('id'); if (window.playlist.length>1) { window.alb_num = $('li#'+window.number).attr('alb_num'); $('#list').animate({scrollTop:($('li#'+window.number).offset().top-$('li#0').offset().top)},200); } $('li#'+window.number).attr('style','font-weight:bold; font-style:italic'); $('#my_audio').trigger('pause'); // #my_audio - html5  audio.   . var track = window.playlist[window.number]; $('#title').html('<h3>'+(Number(window.number)+1)+': '+track.title+' ('+track.artist+')</h3>'); $('#my_audio').attr('src',track.mp3); //    $('#my_audio').trigger('play'); //   }); }); }); 


Tips:

 //   jquery.autocomplete $('#query').autocomplete({serviceUrl:'/mp3player/hint'}); 


Volume change:

 $('#volume_slider').slider({orientation:'vertical',range:'min',min:0,max:100,value:100,stop:function(event,ui){ var volume = (ui.value*1.0)/100.0; $('#my_audio').prop('volume',volume); } }); 


Track Wandering:

 $('#time_slider').slider({disabled:true,range:'min',min:0,max:1000,stop:function(event, ui) { var dur = $('#my_audio').prop('duration'); var cur = (dur*ui.value)/1000; $('#my_audio').prop('currentTime',cur); } }); 


Interactive display of the current position:

 $('#my_audio').bind('timeupdate',function(){ var cur = $('#my_audio').prop('currentTime'); var dur = $('#my_audio').prop('duration'); var left = dur - cur; if (dur) { var slider_val = cur*1000/dur; cur = Math.floor(cur+0.5); dur = Math.floor(dur+0.5); left = Math.floor(left+0.5); cur_s = cur % 60; cur_m = (cur - cur_s)/60; dur_s = dur % 60; dur_m = (dur - dur_s)/60; left_s = left % 60; left_m = (left - left_s)/60; cur_s = $.formatNumber(cur_s,{format:'00',locale:'ru'}); cur_m = $.formatNumber(cur_m,{format:'00',locale:'ru'}); dur_s = $.formatNumber(dur_s,{format:'00',locale:'ru'}); dur_m = $.formatNumber(dur_m,{format:'00',locale:'ru'}); left_s = $.formatNumber(left_s,{format:'00',locale:'ru'}); left_m = $.formatNumber(left_m,{format:'00',locale:'ru'}); $('#time_cur').text(cur_m+':'+cur_s+' ') $('#time_dur').text(' '+left_m+':'+left_s); $('#time_slider').slider('option',{disabled:false}); $('#time_slider').slider('value',slider_val); } }); 


Switch to the next track at the end:

 $('#my_audio').on('ended',function(){ var n = (Number(window.number) + 1) % window.playlist.length; $('li#'+n).trigger('click'); }); 


The finished player can be seen here:
http://home.tabatsky.ru/mp3player/homeaudio/desktop.jsp

4. Audio server


It so happened that good speakers and one device on an android were gathering dust around me. As a result, there was an idea to write an audio server for android and listen to music through speakers.

The audio server consists of one Activity and two services - HttpService and PlayerService .

And so, in more detail.

HttpService accepts HTTP requests and sends PlayerService commands.

 public int onStartCommand(Intent intent, int flags, int startId) { t = new Thread() { public void run() { try { ss = new ServerSocket(port, backlog, InetAddress.getByName(addr)); while (true) { //     Socket s = ss.accept(); Thread tt = new Thread(new SocketProcessor(s)); tt.start(); tt.join(50); } } catch (Throwable e) { e.printStackTrace(); } } }; t.start(); // BroadcastReceiver   c PlayerService filter = new IntentFilter("Http"); receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String res = intent.getStringExtra("result"); if (res!=null) result = "'"+res+"'"; String stat = intent.getStringExtra("status"); if (stat!=null) status = "'"+stat+"'"; String cur = intent.getStringExtra("currentTime"); if (cur!=null) currentTime = cur; String dur = intent.getStringExtra("duration"); if (dur!=null) duration = dur; } }; registerReceiver(receiver, filter); return START_STICKY; } //     HTTP- private class SocketProcessor implements Runnable { private Socket s; private InputStream is; private OutputStream os; private SocketProcessor(Socket s) throws Throwable { this.s = s; this.is = s.getInputStream(); this.os = s.getOutputStream(); } public void run() { try { //   HTTP-: readInputHeaders(); //        jsonp: String response = ""; response += "window.result="+result+"; "; response += "window.status="+status+"; "; response += "window.currentTime="+currentTime+"; "; response += "window.duration="+duration+"; "; writeResponse(response); } catch (Throwable t) { /*do nothing*/ } finally { try { s.close(); } catch (Throwable t) { /*do nothing*/ } } System.err.println("Client processing finished"); } private void writeResponse(String s) throws Throwable { String response = "HTTP/1.1 200 OK\r\n" + "Server: YarServer/2009-09-09\r\n" + "Content-Type: text/javascript\r\n" + "Content-Length: " + s.length() + "\r\n" + "Connection: close\r\n\r\n"; String result = response + s; os.write(result.getBytes()); os.flush(); } private void readInputHeaders() throws Throwable { BufferedReader br = new BufferedReader(new InputStreamReader(is)); String data = ""; String action = ""; String src = ""; String volume = ""; while(true) { String s = br.readLine(); //System.err.println(s); //data += s+"\n"; if (s.startsWith("GET /favicon.ico")) return; if (s.startsWith("GET /?")) { s = s.replace("GET /?", "").replace(" HTTP/1.1", ""); String[] arr = s.split("&"); for (String str: arr) { str = URLDecoder.decode(str, "UTF-8"); if (str.startsWith("action=")) action = str.replace("action=", ""); if (str.startsWith("src=")) src = str.replace("src=", ""); if (str.startsWith("volume=")) volume = str.replace("volume=", ""); } } if(s == null || s.trim().length() == 0) { break; } } //  broadcast  Intent in = new Intent("Player"); in.putExtra("action", action); in.putExtra("src",src); in.putExtra("volume", volume); sendBroadcast(in); } } } 


PlayerService is responsible for playing music:

 public int onStartCommand(Intent intent, int flags, int startId) { player = new MediaPlayer(); player.setAudioStreamType(AudioManager.STREAM_MUSIC); player.setOnPreparedListener(new OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mp.start(); result = "ok"; status = "playing"; } }); player.setOnCompletionListener(new OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { //player.stop(); result = "ok"; if (mp.getCurrentPosition()>mp.getDuration()-2500) status = "finished"; } }); //   HttpService filter = new IntentFilter("Player"); receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getStringExtra("action"); if (action.equals("play")) { player.start(); status = "playing"; } if (action.equals("pause")) { player.pause(); status = "paused"; } if (action.equals("changesrc")) { src = intent.getStringExtra("src"); try { status = "preparing"; player.reset(); //host -      player.setDataSource(host+src); player.prepare(); //player.start(); result = "ok"; } catch (Exception e) { result = "error"; } } if (action.equals("setvolume")) { float volume = Float.parseFloat(intent.getStringExtra("volume")); player.setVolume(volume, volume); } } }; registerReceiver(receiver, filter); // 100     HttpService t = new Thread() { public void run() { while (true) { in = new Intent("Http"); in.putExtra("result", result); //in.putExtra("status", (player.isPlaying()?"playing":"paused")); in.putExtra("status", status); in.putExtra("currentTime", Integer.valueOf(player.getCurrentPosition()/1000).toString()); in.putExtra("duration", Integer.valueOf(player.getDuration()/1000).toString()); sendBroadcast(in); try { sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block //e.printStackTrace(); } } } }; t.start(); return START_STICKY; } 


It remains only to slightly change the corresponding front-end code.

 //  $('li.track').click(function(){ ........ // window.audioServer -     var url = window.audioServer+'/?action=changesrc&src='+track.mp3+'&t='+(new Date().getTime()); $.ajax({ url: encodeURI(url), type: 'GET', crossDomain: true, dataType: 'jsonp' }); ......... } //  $('#volume_slider').slider({orientation:'vertical',range:'min',min:0,max:100,value:100,stop:function(event,ui){ var volume = (ui.value*1.0)/100.0; //$('#my_audio').prop('volume',volume); window.setVolume(volume); } }); window.setVolume = function(volume) { var url = window.audioServer+'/?action=setvolume&volume='+volume +'&t='+(new Date().getTime()); $.ajax({ url: encodeURI(url), type: 'GET', crossDomain: true, dataType: 'jsonp' }); }; //   window.startUpdate = function() { window.update_interval = setInterval(function(){ var url = window.audioServer+'/?action=update'+'&t='+(new Date().getTime());; $.ajax({ url: encodeURI(url), type: 'GET', crossDomain: true, dataType: 'jsonp' }); if (window.result=='error') { //alert(''); } else if (window.result=='ok') { if (window.status=='playing') { if (!window.fin) { window.fin = 0; } else { window.fin--; } window.updateTime(window.currentTime, window.duration); } else if ((window.fin==0)&&(window.status=='finished')) { window.fin = 2; $('#fwd').trigger('click'); } } },500); }; 


Bottom line: playing music on the speakers can be controlled from any device connected to your home network.

Source: https://habr.com/ru/post/239355/


All Articles