©  Jan Newmarch 2017

Jan Newmarch, Linux Sound Programming, 10.1007/978-1-4842-2496-0_24

24. MP3+G

Jan Newmarch

(1)Oakleigh, Victoria, Australia

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.

In Chapter 23, I 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 these two files are zipped together.

Files can be extracted from CDG karaoke discs by using cdrdao and cdgrip.py. They can be played by VLC when given the MP3 file as an argument. It will 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, you will consider how to list and play them, along with keeping lists of favorite songs. The chapter looks at a Java application to perform this and is really just standard Swing 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), and so on. 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 Figure 24-1.

A435426_1_En_24_Fig1_HTML.gif
Figure 24-1. Client requesting songs on an HTTP server to play on PC

The Java client application for Linux and Windows looks like Figure 24-2.

A435426_1_En_24_Fig2_HTML.jpg
Figure 24-2. User interface on client

This shows the main window of songs and on its right the favorites 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 Figure 24-3.

A435426_1_En_24_Fig3_HTML.jpg
Figure 24-3. Songs by Sting

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 Organization

If MP3+G songs are ripped from CDG karaoke discs, then a natural organization would be to store the files in directories, with each directory corresponding to one disc. You could give even more structure by grouping the directories by common artist, by style of music, and so on. You can 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 files needs to be supplied to the clients. After a bit of experimentation, a Vector of SongInformation is created and serialized using Java’s object serialization methods. The serialized file is also kept on the HTTP server. When a client starts up, it gets this file from the HTTP server and deserializes it.

Building this vector means walking the directory tree on the HTTP server and recording information as it goes. The Java code to walk directory trees is fairly straightforward. It is a little tedious if you want it to be OS independent, but Java 1.7 introduced mechanisms to make this easier. These belong to the New I/O (NIO.2) system. The first class of importance is 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 with the following:

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

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

public FileVisitResult visitFile(Path file, BasicFileAttributes attr)

The traversal is done with the following:

Visitor pf = new Visitor();
Files.walkFileTree(startingDir, pf);

A full explanation of this is given on the Java Tutorials site in “Walking the File Tree” ( http://docs.oracle.com/javase/tutorial/essential/io/walk.html ). You use this to load all song information from disc 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 file path of the song. In my current setup, the files look like this:

/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 straightforward pattern matching code can extract these parts, as shown here:

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 whether a title matches, use this:

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” ( www.vogella.com/articles/JavaRegularExpressions/article.html ) by Lars Vogel for more details.

The following is the complete SongInformation file:

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 SongTablebuilds 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 you do not use the Java Persistence API ( http://en.wikibooks.org/wiki/Java_Persistence/What_is_Java_persistence%3F ). Designed for J2EE, it is too heavyweight for our purposes 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 as follows:

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 {


            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 {
            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() + " ");
        }
        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);
    }
}

Favorites

I’ve built this system for my home environment system, and I have a regular group of friends visit. We each have our favorite songs to sing, so we have made up lists on scraps of paper that get lost, have wine spilled on them, and so on. So, this system includes a favorites list of songs.

Each favorites 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:

  • Add song to list

  • Delete song from list

  • Play song

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 favorites 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; this is covered later in the chapter.

Favorites 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 favorites file is saved on each change to the server.

The following is the code for Favourites:

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 {
            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 Favorites

There’s nothing special here. It just loads the tables for each person and builds a Favourites object that it places in a JTabbedPane. It also adds a New tab for adding more users.

The code for AllFavouritesis as follows:

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("/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 the patterns matched. The originally loaded table is kept for restoration and patching matching. The code for SongTableSwing is as follows:

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 bash shell pipeline such as the following:

#!/bin/bash
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 plug-ins to enhance its basic core functionality. You saw in an earlier chapter that if a directory contains both an MP3 file and a CDG file with the same base name, 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 plug-in to adjust speed (while keeping the pitch unchanged). This plug-in can be accessed by the Lua interface to VLC. Once it’s set up, you can send commands such as the following across standard input from the process that started VLC (such as a command-line shell):

rate 1.04

This will change the speed and leave the pitch unchanged.

Setting up VLC to accept Lua commands from stdin can be done with the following command options:

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

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

Currently, it is not simple to add pitch control to VLC. Take a deep breath.

  • Turn off PulseAudio and start Jack.

  • Run jack-rack and install the TAP_pitch filter.

  • Run VLC with Jack output.

  • Using qjackctl, hook VLC to output through jack-rack, which outputs to a system.

  • Control pitch through the jack-rack GUI.

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. This is a distributed system.

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

sshfs -o idmap=user -o rw -o allow_other [email protected]:/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 this:

java SongTableSwing | client 192.168.1.7

On the (Raspberry Pi/CubieBoard) server, I run this:

#!/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 ", 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);
    }
}

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] = '';
            fputs(buf, stdout);
            fflush(stdout);
        }
        close(client_sockfd);
    }
}

Conclusion

This chapter showed how to build a player for MP3+G files.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset