Upto: Table of Contents of full book "Programming and Using Linux Sound"

MP3+G

This chapter explores using Karaoke files in MP3+G format. Files are pulled off a server to a (small) computer attached to a display device (my TV). Files are chosen using a Java Swing application running on Linux or Windows, or by an Android application.

Files

Source files in this chapter are here .

Introduction

In the chapter on User level Karaoke tools we discussed the MP3+G format for Karaoke. Each "song" consists of two files: an MP3 file for the audio and a low-quality CDG file for the video (mainly the lyrics). Often this pair of files are zipped together.

Files can be extracted from CDG Karaoke disks by using cdrdao and cdgrip.py. They can be played by vlc when given the MP3 file as argument - it wil pick up the CDG file from the same directory.

Many people will have built up a sizeable collection of MP3+G songs. In this chapter we consider how to list and play them, along with keeping lists of favourite songs. The chapter just looks at a Java application to perform this, and is really just standard Swing and Android programming. There are no special audio or Karaoke features considered in this chapter.

I keep my files on a server. I can access them in many ways on the other computers in the house: SAMBA shares, HTTP downloads, SSH file system (sshfs), etc. Some mechanisms are less portable than others; for example sshfs is not a standard Windows application and SMB/SAMBA is not a standard Android client. So after getting everything working using sshfs (a no-brainer under standard Linux), I then converted the applications to HTTP access. This has its own wrinkles :-).

The environment looks like

The Java client application for Linux and Windows looks like

MP3+G player
MP3+G player

This shows the main window of songs and on its right the favourites window for two people, Jan and Linda. The application handles multiple languages - english, korean and chinese are shown.

Filters can be applied to the main song list. For example, filtering on the singer Sting gives

Songs by Sting
Songs by Sting

The Android application looks like XXX

When Play is clicked, information about the selection is sent to the media player - currently a CubieBoard2 connected to my hifi/TV. The media computer fetches the files from the HTTP server. Files are played on the media computer using vlc as it can handle MP3+G files.

File organisation

If MP3+G songs are ripped from CDG Karaoke disks, then a natural organisation would be to store the files in directories, each directory corresponding to one disk. More structure may be given by grouping the directories by common artist, or by style of music, etc. We just assume a directory structure with music files as leaf nodes. These files are kept on the HTTP server.

I currently have a large number of these files on my server. Information about these needs to be supplied to the clients. After a bit of experimentation a Vector of SongInformation is created and serialised using Java's object serialisation methods. The serialised file is also kept on the HTTP server. When a client starts up, it gets this file from the HTTP server and deserialises it.

Building this vector means walking the directory tree on the HTTP server and recording information as it goes. Java code to walk directory trees is fairly straightforward. It is a little tedious if you want it to be O/S independent. Java 1.7 introduced mechanisms to make this easier. These belong to the New I/O (NIO) system. The first class of importance is the java.nio.file.Path which "[is] an object that may be used to locate a file in a file system. It will typically represent a system dependent file path." A string representing a file location in, say, a Linux or a Windows file system can be turned into a Path object by

Path startingDir = FileSystems.getDefault().getPath(dirString);
      

Traversal of a file system from a given path is done by walking a file tree, calling a node "visitor" at each point. The visitor is a subclass of SimpleFileVisitor<Path> and for leaf nodes only you would override the method

public FileVisitResult visitFile(Path file, BasicFileAttributes attr)
      
The traversal is done by
Visitor pf = new Visitor();
Files.walkFileTree(startingDir, pf);
      

A full explanation of this is given in the Java Tutorials on Walking the File Tree . We use this to load all song information from disk into a vector of song paths in SongTable.java.

Song information

The information about each song should include its path in the file system, the name of the artist(s), the title of the song and any other useful information. This information has to be pulled out of the the file path of the song. In my current setup, files look like

	
/server/KARAOKE/Sonken/SK-50154 - Crosby, Stills - Carry On.mp3
	
      

Each song has a reasonably unique identifier ("SK-50154"), a unique path and an artist and title. Reasonably straight-forward pattern matching code can extract these parts:

Path file = ...
String fname = file.getFileName().toString();
if (fname.endsWith(".zip") || 
    fname.endsWith(".mp3")) {
    String root = fname.substring(0, fname.length()-4);
    String parts[] = root.split(" - ", 3);
    if (parts.length != 3)
        return;

	String index = parts[0];
	String artist = parts[1];
	String title = parts[2];

        SongInformation info = new SongInformation(file,
						   index,
						   title,
						   artist);
      

(The patterns produced by cdrip.py are not quite the same, but the code is easily changed.)

The SongInformation class captures this information and also includes methods for pattern matching of a string against the various fields. For example, to check if a title matches,

public boolean titleMatch(String pattern) {
    return title.matches("(?i).*" + pattern + ".*");
}
      

This gives a case-independent match using Java regular expression support. See Java Regex Tutorial by Lars Vogel for more details.

The complete SongInformation file is


import java.nio.file.Path;
import java.io.Serializable;

public class SongInformation implements Serializable {


    // Public fields of each song record

    public String path;

    public String index;

    /**
     * song title in Unicode
     */
    public String title;

    /**
     * artist in Unicode
     */
    public String artist;



    public SongInformation(Path path,
			   String index,
			   String title,
			   String artist) {
	this.path = path.toString();
	this.index = index;
	this.title = title;
	this.artist = artist;
    }

    public String toString() {
	return "(" + index + ") " + artist + ": " + title;
    }

    public boolean titleMatch(String pattern) {
	return title.matches("(?i).*" + pattern + ".*");
    }

    public boolean artistMatch(String pattern) {
	return artist.matches("(?i).*" + pattern + ".*");
    }

    public boolean numberMatch(String pattern) {
	return index.equals(pattern);
    }
}

      

Song table

The SongTable builds up a vector of SongInformation objects by traversing the file tree.

If there are many songs (say, in the thousands) this can lead to a slow startup time. To reduce this, once a table is loaded, it is saved to disk as a persistent object by writing it to an ObjectOutputStream. The next time the program is started, an attempt is made to read it back from this using an ObjectInputStream. Note that we do not use the Java Persistence API - designed for J2EE, it is too heavyweight for our purpose here.

The SongTable also includes code to build smaller song tables based on matches between patterns and the title (or artist or number). It can search for matches between a pattern and a song and build a new table based on the matches. It contains a pointer to the original table for restoration later. This allows searches for patterns to use the same data structure.

The code for SongTable is


import java.util.Vector;
import java.io.FileInputStream;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.FileSystems;
import java.nio.file.attribute.*;


class Visitor
    extends SimpleFileVisitor<Path> {

    private Vector<SongInformation> songs;

    public Visitor(Vector<SongInformation> songs) {
	this.songs = songs;
    }

    @Override
    public FileVisitResult visitFile(Path file,
                                   BasicFileAttributes attr) {
	if (attr.isRegularFile()) {
	    String fname = file.getFileName().toString();
	    //System.out.println("Regular file " + fname);
	    if (fname.endsWith(".zip") || 
		fname.endsWith(".mp3") || 
		fname.endsWith(".kar")) {
		String root = fname.substring(0, fname.length()-4);
		//System.err.println(" root " + root);
		String parts[] = root.split(" - ", 3);
		if (parts.length != 3)
		    return java.nio.file.FileVisitResult.CONTINUE;

		String index = parts[0];
		String artist = parts[1];
		String title = parts[2];

		SongInformation info = new SongInformation(file,
							   index,
							   title,
							   artist);
		songs.add(info);
	    }
	}

        return java.nio.file.FileVisitResult.CONTINUE;
    }
}

public class SongTable {

    private static final String SONG_INFO_ROOT = "/server/KARAOKE/KARAOKE/";

    private static Vector<SongInformation> allSongs;

    public Vector<SongInformation> songs = 
	new Vector<SongInformation>  ();

    public static long[] langCount = new long[0x23];

    public SongTable(Vector<SongInformation> songs) {
	this.songs = songs;
    }

    public SongTable(String[] args) throws java.io.IOException, 
					   java.io.FileNotFoundException {
	if (args.length >= 1) {
	    System.err.println("Loading from " + args[0]);
	    loadTableFromSource(args[0]);
	    saveTableToStore();
	} else {
	    loadTableFromStore();
	}
    }

    private boolean loadTableFromStore() {
	try {
	    /*
	    String userHome = System.getProperty("user.home");
	    Path storePath = FileSystems.getDefault().getPath(userHome, 
							      ".karaoke",
							      "SongStore");
	    
	    File storeFile = storePath.toFile();
	    */
	    File storeFile = new File("/server/KARAOKE/SongStore"); 
	    
	    FileInputStream in = new FileInputStream(storeFile); 
	    ObjectInputStream is = new ObjectInputStream(in);
	    songs = (Vector<SongInformation>) is.readObject();
	    in.close();
	} catch(Exception e) {
	    System.err.println("Can't load store file " + e.toString());
	    return false;
	}
	return true;
    }

    private void saveTableToStore() {
	try {
	    /*
	    String userHome = System.getProperty("user.home");
	    Path storePath = FileSystems.getDefault().getPath(userHome, 
							      ".karaoke",
							      "SongStore");
	    File storeFile = storePath.toFile();
	    */
	    File storeFile = new File("/server/KARAOKE/SongStore");
	    FileOutputStream out = new FileOutputStream(storeFile); 
	    ObjectOutputStream os = new ObjectOutputStream(out);
	    os.writeObject(songs); 
	    os.flush(); 
	    out.close();
	} catch(Exception e) {
	    System.err.println("Can't save store file " + e.toString());
	}
    }

    private void loadTableFromSource(String dir) throws java.io.IOException, 
			      java.io.FileNotFoundException {

	Path startingDir = FileSystems.getDefault().getPath(dir);
	Visitor pf = new Visitor(songs);
	Files.walkFileTree(startingDir, pf);
    }

    public java.util.Iterator<SongInformation> iterator() {
	return songs.iterator();
    }
 
    public SongTable titleMatches( String pattern) {
	Vector<SongInformation> matchSongs = 
	    new Vector<SongInformation>  ();

	for (SongInformation song: songs) {
	    if (song.titleMatch(pattern)) {
		matchSongs.add(song);
	    }
	}
	return new SongTable(matchSongs);
    }

     public SongTable artistMatches( String pattern) {
	Vector<SongInformation> matchSongs = 
	    new Vector<SongInformation>  ();

	for (SongInformation song: songs) {
	    if (song.artistMatch(pattern)) {
		matchSongs.add(song);
	    }
	}
	return new SongTable(matchSongs);
    }

    public SongTable numberMatches( String pattern) {
	Vector<SongInformation> matchSongs = 
	    new Vector<SongInformation>  ();

	for (SongInformation song: songs) {
	    if (song.numberMatch(pattern)) {
		matchSongs.add(song);
	    }
	}
	return new SongTable(matchSongs);
    }

    public String toString() {
	StringBuffer buf = new StringBuffer();
	for (SongInformation song: songs) {
	    buf.append(song.toString() + "\n");
	}
	return buf.toString();
    }
	
    public static void main(String[] args) {
	// for testing
	SongTable songs = null;
	try {
	    songs = new SongTable(new String[] {SONG_INFO_ROOT});
	} catch(Exception e) {
	    System.err.println(e.toString());
	    System.exit(1);
	}

	System.out.println(songs.artistMatches("Tom Jones").toString());

	System.exit(0);
    }
}

      

Favourites

I've built this system for my home environment where I have a regular group of friends visiting. We each have our favourite songs to sing and so we have made up lists on scraps of paper which get lost, have wine spilt on them, etc. So this system includes favourite lists of songs.

Each favourites list is essentially just another SongTable. But I have put a JList around the table to display it. The JList uses a DefaultListModel, and the constructor loads a song table into this list by iterating through the table and adding elements

	int n = 0;
	java.util.Iterator<SongInformation> iter = favouriteSongs.iterator();
	while(iter.hasNext()) {
	    model.add(n++, iter.next());
	}
      

Other Swing code adds three buttons along the bottom:

Adding a song to the list means taking the selected item from the main song table and adding it to this table. The main table is passed into the constructor and just kept for the purpose of getting its selection. The selected object is added to both the Swing JList and to the favourites SongTable.

"Playing a song" is done in a simple way: the full path to the song is written to standard output, newline terminated. Another program in a pipeline can then pick this up - see later.

Favourites aren't much good if they don't persist from one day to the next! So the same object storage method as before is used as with the full song table. Each favourites file is saved on each change. There are some Linux/Unix dependencies here, in that application information is stored in a subdirectory beginning with a "." in the user's home directory.

The code for Favourites is


import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import javax.swing.*;
import javax.swing.SwingUtilities;
import java.util.regex.*;
import java.io.*;
import java.nio.file.FileSystems;
import java.nio.file.*;

public class Favourites extends JPanel {
    private DefaultListModel model = new DefaultListModel();
    private JList list;

    // whose favoutites these are
    private String user;

    // songs in this favourites list
    private final SongTable favouriteSongs;

    // pointer back to main song table list
    private final SongTableSwing songTable;

    // This font displays Asian and European characters.
    // It should be in your distro.
    // Fonts displaying all Unicode are zysong.ttf and Cyberbit.ttf
    // See http://unicode.org/resources/fonts.html
    private Font font = new Font("WenQuanYi Zen Hei", Font.PLAIN, 16);
    
    private int findIndex = -1;

    public Favourites(final SongTableSwing songTable, 
		      final SongTable favouriteSongs, 
		      String user) {
	this.songTable = songTable;
	this.favouriteSongs = favouriteSongs;
	this.user = user;

	if (font == null) {
	    System.err.println("Can't find font");
	}
		
	int n = 0;
	java.util.Iterator<SongInformation> iter = favouriteSongs.iterator();
	while(iter.hasNext()) {
	    model.add(n++, iter.next());
	}

	BorderLayout mgr = new BorderLayout();
 
	list = new JList(model);
	list.setFont(font);
	JScrollPane scrollPane = new JScrollPane(list);

	setLayout(mgr);
	add(scrollPane, BorderLayout.CENTER);

	JPanel bottomPanel = new JPanel();
	bottomPanel.setLayout(new GridLayout(2, 1));
	add(bottomPanel, BorderLayout.SOUTH);

	JPanel searchPanel = new JPanel();
	bottomPanel.add(searchPanel);
	searchPanel.setLayout(new FlowLayout());

	JPanel buttonPanel = new JPanel();
	bottomPanel.add(buttonPanel);
	buttonPanel.setLayout(new FlowLayout());

	JButton addSong = new JButton("Add song to list");
	JButton deleteSong = new JButton("Delete song from list");
	JButton play = new JButton("Play");

	buttonPanel.add(addSong);
	buttonPanel.add(deleteSong);
	buttonPanel.add(play);

	play.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    playSong();
		}
	    });

	deleteSong.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    SongInformation song = (SongInformation) list.getSelectedValue();
		    model.removeElement(song);
		    favouriteSongs.songs.remove(song);
		    saveToStore();
		}
	    });

	addSong.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    SongInformation song = songTable.getSelection();
		    model.addElement(song);
		    favouriteSongs.songs.add(song);
		    saveToStore();
		}
	    });
     }

    private void saveToStore() {
	try {
	    /*
	    String userHome = System.getProperty("user.home");
	    Path storePath = FileSystems.getDefault().getPath(userHome, 
							      ".karaoke",
							      "favourites",
							      user);
	    File storeFile = storePath.toFile();
	    */
	    File storeFile = new File("/server/KARAOKE/favourites/" + user);
	    FileOutputStream out = new FileOutputStream(storeFile); 
	    ObjectOutputStream os = new ObjectOutputStream(out);
	    os.writeObject(favouriteSongs.songs); 
	    os.flush(); 
	    out.close();
	} catch(Exception e) {
	    System.err.println("Can't save favourites file " + e.toString());
	}
    }


    /**
     * "play" a song by printing its file path to standard out.
     * Can be used in a pipeline this way
     */
    public void playSong() {
	SongInformation song = (SongInformation) list.getSelectedValue();
	if (song == null) {
	    return;
	}
	System.out.println(song.path.toString());
    }


    class SongInformationRenderer extends JLabel implements ListCellRenderer {

	public Component getListCellRendererComponent(
						      JList list,
						      Object value,
						      int index,
						      boolean isSelected,
						      boolean cellHasFocus) {
	    setText(value.toString());
	    return this;
	}
    }
}

      

All favourites

There's nothing special here. It just loads the tables for each person and builds a Favourites object which it places in a JTabbedPane. It also adds in a "NEW" tab to add additional users.

The code for AllFavourites is

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.Vector;
import java.nio.file.*;
import java.io.*;

public class AllFavourites extends JTabbedPane {
    private SongTableSwing songTable;

    public AllFavourites(SongTableSwing songTable) {
	this.songTable = songTable;

	loadFavourites();

	NewPanel newP = new NewPanel(this);
	addTab("NEW", null, newP);
    }

    private void loadFavourites() {
	String userHome = System.getProperty("user.home");
	/*
	Path favouritesPath = FileSystems.getDefault().getPath(userHome, 
							    ".karaoke",
							    "favourites");
	*/
	Path favouritesPath = FileSystems.getDefault().getPath("/server/KARAOKE/favourites");
	try {
	    DirectoryStream<Path> stream = 
		Files.newDirectoryStream(favouritesPath);
	    for (Path entry: stream) {
		int nelmts = entry.getNameCount();
		Path last = entry.subpath(nelmts-1, nelmts);
		System.err.println("Favourite: " + last.toString());
		File storeFile = entry.toFile();
		
		FileInputStream in = new FileInputStream(storeFile); 
		ObjectInputStream is = new ObjectInputStream(in);
		Vector<SongInformation> favouriteSongs = 
		    (Vector<SongInformation>) is.readObject();
		in.close();
		for (SongInformation s: favouriteSongs) {
		    System.err.println("Fav: " + s.toString());
		}

		SongTable favouriteSongsTable = new SongTable(favouriteSongs);
		Favourites f = new Favourites(songTable, 
					      favouriteSongsTable, 
					      last.toString());
		addTab(last.toString(), null, f, last.toString());
		System.err.println("Loaded favs " + last.toString());
	    }
	} catch(Exception e) {
	    System.err.println(e.toString());
	}
    }

    class NewPanel extends JPanel {
	private JTabbedPane pane;

	public NewPanel(final JTabbedPane pane) {
	    this.pane = pane;

	    setLayout(new FlowLayout());
	    JLabel nameLabel = new JLabel("Name of new person");
	    final JTextField nameField = new JTextField(10);
	    add(nameLabel);
	    add(nameField);

	    nameField.addActionListener(new ActionListener(){
		    public void actionPerformed(ActionEvent e){
			String name = nameField.getText();

			SongTable songs = new SongTable(new Vector<SongInformation>());
			Favourites favs = new Favourites(songTable, songs, name);
			
			pane.addTab(name, null, favs);
		    }});

	}
    }
}

      

Swing song table

This is mainly code to get the different song tables loaded and to buld the Swing interface. It also filters the showing table based on patterns matched. The originally loaded table is kept for restoration and patching matching. The code for SongTableSwing is


import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import javax.swing.*;
import javax.swing.SwingUtilities;
import java.util.regex.*;
import java.io.*;

public class SongTableSwing extends JPanel {
   private DefaultListModel model = new DefaultListModel();
    private JList list;
    private static SongTable allSongs;

    private JTextField numberField;
    private JTextField langField;
    private JTextField titleField;
    private JTextField artistField;

    // This font displays Asian and European characters.
    // It should be in your distro.
    // Fonts displaying all Unicode are zysong.ttf and Cyberbit.ttf
    // See http://unicode.org/resources/fonts.html
    private Font font = new Font("WenQuanYi Zen Hei", Font.PLAIN, 16);
    // font = new Font("Bitstream Cyberbit", Font.PLAIN, 16);
    
    private int findIndex = -1;

    /**
     * Describe <code>main</code> method here.
     *
     * @param args a <code>String</code> value
     */
    public static final void main(final String[] args) {
	if (args.length >= 1 && 
	    args[0].startsWith("-h")) {
	    System.err.println("Usage: java SongTableSwing [song directory]");
	    System.exit(0);
	}

	allSongs = null;
	try {
	    allSongs = new SongTable(args);
	} catch(Exception e) {
	    System.err.println(e.toString());
	    System.exit(1);
	}

	JFrame frame = new JFrame();
	frame.setTitle("Song Table");
	frame.setSize(700, 800);
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	
	SongTableSwing panel = new SongTableSwing(allSongs);
	frame.getContentPane().add(panel);

	frame.setVisible(true);

	JFrame favourites = new JFrame();
	favourites.setTitle("Favourites");
	favourites.setSize(600, 800);
	favourites.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	
	AllFavourites lists = new AllFavourites(panel);
	favourites.getContentPane().add(lists);

	favourites.setVisible(true);

    }

    public SongTableSwing(SongTable songs) {

	if (font == null) {
	    System.err.println("Can't fnd font");
	}
		
	int n = 0;
	java.util.Iterator<SongInformation> iter = songs.iterator();
	while(iter.hasNext()) {
	    model.add(n++, iter.next());
	    // model.add(n++, iter.next().toString());
	}

	BorderLayout mgr = new BorderLayout();
 
	list = new JList(model);
	// list = new JList(songs);
	list.setFont(font);
	JScrollPane scrollPane = new JScrollPane(list);

	// Support DnD
	list.setDragEnabled(true);

	setLayout(mgr);
	add(scrollPane, BorderLayout.CENTER);

	JPanel bottomPanel = new JPanel();
	bottomPanel.setLayout(new GridLayout(2, 1));
	add(bottomPanel, BorderLayout.SOUTH);

	JPanel searchPanel = new JPanel();
	bottomPanel.add(searchPanel);
	searchPanel.setLayout(new FlowLayout());

	JLabel numberLabel = new JLabel("Number");
	numberField = new JTextField(5);

	JLabel langLabel = new JLabel("Language");
	langField = new JTextField(8);

	JLabel titleLabel = new JLabel("Title");
	titleField = new JTextField(20);
	titleField.setFont(font);

	JLabel artistLabel = new JLabel("Artist");
	artistField = new JTextField(10);
	artistField.setFont(font);

	searchPanel.add(numberLabel);
	searchPanel.add(numberField);
	searchPanel.add(titleLabel);
	searchPanel.add(titleField);
	searchPanel.add(artistLabel);
	searchPanel.add(artistField);

	titleField.getDocument().addDocumentListener(new DocumentListener() {
		public void changedUpdate(DocumentEvent e) {
		    // rest find to -1 to restart any find searches
		    findIndex = -1;
		    // System.out.println("reset find index");
		}
		public void insertUpdate(DocumentEvent e) {
		    findIndex = -1;
		    // System.out.println("reset insert find index");
		}
		public void removeUpdate(DocumentEvent e) {
		    findIndex = -1;
		    // System.out.println("reset remove find index");
		}
	    }
	    );
	artistField.getDocument().addDocumentListener(new DocumentListener() {
		public void changedUpdate(DocumentEvent e) {
		    // rest find to -1 to restart any find searches
		    findIndex = -1;
		    // System.out.println("reset insert find index");
		}
		public void insertUpdate(DocumentEvent e) {
		    findIndex = -1;
		    // System.out.println("reset insert find index");
		}
		public void removeUpdate(DocumentEvent e) {
		    findIndex = -1;
		    // System.out.println("reset insert find index");
		}
	    }
	    );

	titleField.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent e){
		    filterSongs();
                }});
	artistField.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent e){
		    filterSongs();
                }});

	JPanel buttonPanel = new JPanel();
	bottomPanel.add(buttonPanel);
	buttonPanel.setLayout(new FlowLayout());

	JButton find = new JButton("Find");
	JButton filter = new JButton("Filter");
	JButton reset = new JButton("Reset");
	JButton play = new JButton("Play");
	buttonPanel.add(find);
	buttonPanel.add(filter);
	buttonPanel.add(reset);
	buttonPanel.add(play);

	find.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    findSong();
		}
	    });

	filter.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    filterSongs();
		}
	    });

	reset.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    resetSongs();
		}
	    });

	play.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    playSong();
		}
	    });
 
     }

    public void findSong() {
	String number = numberField.getText();
	String language = langField.getText();
	String title = titleField.getText();
	String artist = artistField.getText();

	if (number.length() != 0) {
	    return;
	}

	for (int n = findIndex + 1; n < model.getSize(); n++) {
	    SongInformation info = (SongInformation) model.getElementAt(n);

	    if ((title.length() != 0) && (artist.length() != 0)) {
		if (info.titleMatch(title) && info.artistMatch(artist)) {
			findIndex = n;
			list.setSelectedIndex(n);
			list.ensureIndexIsVisible(n);
			break;
		}
	    } else {
		if ((title.length() != 0) && info.titleMatch(title)) {
		    findIndex = n;
		    list.setSelectedIndex(n);
		    list.ensureIndexIsVisible(n);
		    break;
		} else if ((artist.length() != 0) && info.artistMatch(artist)) {
		    findIndex = n;
		    list.setSelectedIndex(n);
		    list.ensureIndexIsVisible(n);
		    break;

		}
	    }

	}
    }

    public void filterSongs() {
	String title = titleField.getText();
	String artist = artistField.getText();
	String number = numberField.getText();
	SongTable filteredSongs = allSongs;

	if (allSongs == null) {
	    return;
	}

	if (title.length() != 0) {
	    filteredSongs = filteredSongs.titleMatches(title);
	}
	if (artist.length() != 0) {
	    filteredSongs = filteredSongs.artistMatches(artist);
	}
	if (number.length() != 0) {
	    filteredSongs = filteredSongs.numberMatches(number);
	}

	model.clear();
	int n = 0;
	java.util.Iterator<SongInformation> iter = filteredSongs.iterator();
	while(iter.hasNext()) {
	    model.add(n++, iter.next());
	}
    }

    public void resetSongs() {
	artistField.setText("");
	titleField.setText("");
	model.clear();
	int n = 0;
	java.util.Iterator<SongInformation> iter = allSongs.iterator();
	while(iter.hasNext()) {
	    model.add(n++, iter.next());
	}
    }
    /**
     * "play" a song by printing its file path to standard out.
     * Can be used in a pipeline this way
     */
    public void playSong() {
	SongInformation song = (SongInformation) list.getSelectedValue();
	if (song == null) {
	    return;
	}
	System.out.println(song.path);
    }

    public SongInformation getSelection() {
	return (SongInformation) (list.getSelectedValue());
    }

    class SongInformationRenderer extends JLabel implements ListCellRenderer {

	public Component getListCellRendererComponent(
						      JList list,
						      Object value,
						      int index,
						      boolean isSelected,
						      boolean cellHasFocus) {
	    setText(value.toString());
	    return this;
	}
    }
}
      

Playing songs

Whenever a song is "played" its file path is written to standard output. This makes it suitable for use in a pipeline such as

VLC_OPTS="--play-and-exit --fullscreen"

java  SongTableSwing |
while read line
do
        if expr match "$line" ".*mp3"
        then
                vlc $VLC_OPTS "$line"
        elif expr match "$line" ".*zip"
        then
                rm -f /tmp/karaoke/*
                unzip -d /tmp/karaoke "$line"
                vlc $VLC_OPTS /tmp/karaoke/*.mp3
        fi
done
      

VLC

VLC is an immensely flexible media player. It relies on a large set of plugins to enhance its basic core functionality. We saw in an earlier chapter that if a directory contains both an MP3 and a CDG file with the same basename then by asking it to play the MP3 file it will also show the CDG video.

Common expectations of Karaoke players are that you can adjust the speed and pitch. Currently VLC cannot adjust pitch, but it does have a plugin to adjust speed (while keeping the pitch unchanged). This plugin can be accessed by the Lua interface to VLC. Once set up, you can send commands such as

	
rate 1.04	  
	
      

across standard input from the process that started VLC (such as a command line shell). This will change the speed and leave the pitch unchanged.

Setting up VLC to accept Lua commands from stdin can be done by the command options

	
vlc -I luaintf --lua-intf cli ...
	
      

Note that this takes away the standard GUI controls (menus, etc) and controls VLC from stdin only.

At present, it is not simple to add pitch control to VLC. Take a deep breath:

Playing songs across the network

I actually want to play songs from my server disk to a Raspberry Pi or CubieBoard connected to my TV, and control the play from a netbook sitting on my lap. (Later I will try to get Android code running to do the same.). This is a distributed system.

Mounting server files on a computer is simple: you can use NFS, Samba, ... I am currently using sshfs as in

sshfs -o idmap=user -o rw -o allow_other newmarch@192.168.1.101:/home/httpd/html /server
      

For remote access/control I replace the run command of the last section by a TCP client/server. On the client, controlling the player, I have

java SongTableSwing | client 192.168.1.7
      

while on the (Raspberry Pi/CubieBoard) server I run

#!/bin/bash
set -x
VLC_OPTS="--play-and-exit -f"

server |
while read line
do
	if expr match "$line" ".*mp3"
	then
		vlc $VLC_OPTS "$line"
	elif expr match "$line" ".*zip"
	then
		rm -f /tmp/karaoke/*
		unzip -d /tmp/karaoke "$line"
		vlc $VLC_OPTS /tmp/karaoke/*.mp3
	fi
done
      

The client/server files are just standard TCP files. The client reads a newline-terminated string from standard input and writes it to the server, and the server prints the same line to standard output. Here is client.c:

#include <stdio.h> 
#include <sys/types.h>
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <stdlib.h>
#include <string.h>

#define SIZE 1024 
char buf[SIZE];
#define PORT 13000
int main(int argc, char *argv[]) { 
    int sockfd; 
    int nread; 
    struct sockaddr_in serv_addr; 
    if (argc != 2) { 
	fprintf(stderr, "usage: %s IPaddr\n", argv[0]); 
	exit(1); 
    } 


    while (fgets(buf, SIZE , stdin) != NULL) {
	/* create endpoint */ 
	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { 
	    perror(NULL); exit(2); 
	} 
	/* connect to server */ 
	serv_addr.sin_family = AF_INET; 
	serv_addr.sin_addr.s_addr = inet_addr(argv[1]); 
	serv_addr.sin_port = htons(PORT);
 
	while (connect(sockfd, 
		       (struct sockaddr *) &serv_addr, 
		       sizeof(serv_addr)) < 0) {
	    /* allow for timesouts etc */
	    perror(NULL);
	    sleep(1);
	}
	
	printf("%s", buf);
	nread = strlen(buf);
	/* transfer data and quit */ 
	write(sockfd, buf, nread);
	close(sockfd); 
    }
} 

      

and here is server.c

#include <stdio.h> 
#include <sys/types.h> 
#include <sys/socket.h> 
#include <netinet/in.h> 
#include <stdlib.h>
#include <signal.h>

#define SIZE 1024 
char buf[SIZE]; 
#define TIME_PORT 13000

int sockfd, client_sockfd; 

void intHandler(int dummy) {
    close(client_sockfd);
    close(sockfd);
    exit(1);
}

int main(int argc, char *argv[]) { 
    int sockfd, client_sockfd; 
    int nread, len; 
    struct sockaddr_in serv_addr, client_addr; 
    time_t t; 

    signal(SIGINT, intHandler);
    
    /* create endpoint */ 
    if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { 
	perror(NULL); exit(2); 
    } 
    /* bind address */ 
    serv_addr.sin_family = AF_INET; 
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); 
    serv_addr.sin_port = htons(TIME_PORT); 
    if (bind(sockfd, 
	     (struct sockaddr *) &serv_addr, 
	     sizeof(serv_addr)) < 0) { 
	perror(NULL); exit(3); 
    } 
    /* specify queue */ 
    listen(sockfd, 5); 
    for (;;) { 
	len = sizeof(client_addr); 
	client_sockfd = accept(sockfd, 
			       (struct sockaddr *) &client_addr, 
			       &len); 
	if (client_sockfd == -1) { 
	    perror(NULL); continue; 
	} 
	while ((nread = read(client_sockfd, buf, SIZE-1)) > 0) {
	    buf[nread] = '\0';
	    fputs(buf, stdout);
	    fflush(stdout);
	}
	close(client_sockfd); 
    }
}

      

Conclusion

This chapter has built a player for MP3+G files.


Copyright © Jan Newmarch, jan@newmarch.name
Creative Commons License
"Programming and Using Linux Sound - in depth" by Jan Newmarch is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License .
Based on a work at https://jan.newmarch.name/LinuxSound/ .

If you like this book, please contribute using PayPal

Or Flattr me:
Flattr this book