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

Conclusion

The dying words...

Where did we start?

The starting point for all this was

Where did I get to?

Well, I got part way there. The system ended up consisting of

I haven't ended up with a better Karaoke machine. Nevertheless, the system can play all of the file types, showing the lyrics and playing the music. It doesn't score, doesn't show a video in the background and doesn't show the notes that should be sung against the notes actually sung. Still to come :-(.

The Karaoke files

The Karaoke files are all stored on a server. They could be accessible from Samba, NFS, SSHFS, etc, etc. The only thing I can guarantee across Linux, Windows and Android is HTTP, so I just make the files available through an Apache server.

The files are stored in subdirectories according to their source. For example, the files I ripped from the Sonken disk are in subdirectory KARAOKE/Sonken. These files can be accessed through a simple HTTP GET request, given the URL for the file.

Communicating choices

When a controller chooses a song, the URL needs to be communicated to the playing device. For commonality across Linux/Windows/Android clients, a simple TCP connection is used. The bandwidth is not high, so to avoid possible blocks caused by the server crashing and clients holding TCP connections open, each client opens a TCP connection, just sends a single URL across a connection and then closes it.

The Java controller

Information about each song is stored in a file SongInformation.java:


package newmarch.songtable;

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

public class SongInformation implements Serializable {

    private static final long serialVersionUID = -7465256749074820597L;

    // 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(String path,
			   String index,
			   String title,
			   String artist) {
	this.path = path;
	this.index = index;
	this.title = title;
	this.artist = artist;
    }

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

    public Vector<String> toVector() {
	Vector<String> v = new Vector<String>();
	v.add(index);
	v.add(artist);
	v.add(title);
	// will be hidden field
	v.add(path);

	return v;
    }

    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) {
	if (pattern.length() > 0 && Character.isDigit(pattern.charAt(0))) {
	    // user typed a number only, assume it is
	    // from the Karaoke songs
	    pattern = "SK-" + pattern;
	}
	return index.equalsIgnoreCase(pattern);
    }
}

      

This includes methods to perform Boolean pattern matches on the object.

Multiple songs are stored in a SongTable which is basically a Vector of SongInformation objects. This table is serialisable so that it can be stored and then loaded quickly from the HTTP server. The class contains some messy code to initialise the table if it is serialised from a directory or from a list of files. Apart from that, it contains code to build a (smaller) song table from a set of pattern matches. This is useful from returning tables that meet a pattern such as the artist being the Beatles.

The table is given by SongTable.java:


package newmarch.songtable;

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.*;
import java.net.URL;
import java.net.URLConnection;
import java.net.HttpURLConnection;

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.toString(),
							   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 final String SONG_STORE_DEFAULT = "http://192.168.1.101/KARAOKE/SongStore";
    private static final String SONG_STORE_ROOT = "http://192.168.1.101/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) {
	    if (args[0].startsWith("-s")) {
		loadTableFromStore(SONG_STORE_ROOT + args[0].substring(2));
	    } else {	    
		System.err.println("Loading from " + args[0]);
		loadTableFromSource(args[0]);
		saveTableToStore(args[1]);
	    }
	} else {
	    loadTableFromStore(SONG_STORE_DEFAULT);
	}
    }

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

    public void saveTableToStore(String urlStr) {
	try {
	    URL url = new URL(urlStr);
	    URLConnection connection = url.openConnection();
	    HttpURLConnection httpConnection = (HttpURLConnection) connection;
	    httpConnection.setDoOutput(true);
	    httpConnection.setRequestMethod("PUT");

	    BufferedOutputStream writer = new BufferedOutputStream(connection.getOutputStream());
	    ObjectOutputStream os = new ObjectOutputStream(writer);
	    os.writeObject(songs);
	    os.close();
	    writer.close();
	    int responseCode = httpConnection.getResponseCode();
	    String responseMsg = httpConnection.getResponseMessage();
	    System.out.println("Saved table to " + urlStr + " with code " + responseCode +
			       responseMsg + " size " + songs.size());
	   
	} catch(Exception e) {
	    System.err.println("Can't save store file " + e.toString());
	}
    }

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

	Path path = FileSystems.getDefault().getPath(source);
	File f = path.toFile();
	if (f.isDirectory()) {
	    Visitor pf = new Visitor(songs);
	    Files.walkFileTree(path, pf);
	} else if (f.isFile()) {
	    loadTableFromFile(path);
	}
    }

    private void loadTableFromFile(Path path) throws java.io.IOException {
	BufferedReader in = new BufferedReader(new FileReader(path.toFile()));
	String fname;

	while ((fname = in.readLine()) != null) {
	    // System.out.println(fname);
	    if (fname.endsWith(".zip") || 
		fname.endsWith(".mp3") || 
		fname.endsWith(".kar")) {
		// lose extension
		String root = fname.substring(0, fname.length()-4);
		// lose /.../
		root = root.substring(root.lastIndexOf('/')+1);
		//System.err.println(" root " + root);
		String parts[] = root.split(" - ", 3);
		if (parts.length != 3)
		    continue;

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

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

    public java.util.Iterator<SongInformation> iterator() {
	return songs.iterator();
    }

    public SongInformation getSongAt(int index) {
	if (index < 0 || index >= songs .size()) {
	    return null;
	}
	return songs.elementAt(index);
    }
 
    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);
    }
}

      

Swing song table UI

There is a principal song table containing all the songs. This may have filters applied to it to show, say, only the Beatles songs. In addition, the visitors to my house have built up a collection of "favourite" songs. Each of these is its own song table. The UI for this is the Swing application SongTableSwing.java:

	
package newmarch.songtable;

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.net.Socket;
import java.net.InetAddress;

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 songdir|songfile]");
	    System.err.println("  -sSongStore");
	    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");

	Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
	frame.setSize(800, ((int)screenSize.getHeight())*5/6);
	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,  ((int)screenSize.getHeight())*3/4);
	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();
                }});
	numberField.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() {
	String SERVERIP = "192.168.1.110"; 
	int SERVERPORT = 13000;
	PrintWriter out;
	
	SongInformation song = (SongInformation) list.getSelectedValue();
	if (song == null) {
	    return;
	}
	System.out.println(song.path);

	try {
	    InetAddress serverAddr = InetAddress.getByName(SERVERIP);
	    Socket socket = new Socket(serverAddr, SERVERPORT);
				 
	    //send the message to the server
	    out = new PrintWriter(
				  new BufferedWriter(
						     new OutputStreamWriter(socket.getOutputStream())), 
				  true);
	    // Avoid println to socket for Windows
	    out.print(song.path + "\n");
	    out.flush();
	    socket.close();
				
	} catch (Exception e) {
	    System.err.println(e.toString());
	}
    }

    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;
	}
    }
}

      

The favourites classes are AllFavourites.java:

	package newmarch.songtable;

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

public class AllFavourites extends JTabbedPane {
    public static String FAVOURITES_DIR = "http://192.168.1.101:8000/KARAOKE/favourites/";

    private SongTableSwing songTable;
    public Vector<FavouritesInfo> favourites = new Vector<FavouritesInfo>();

    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");
	*/

	try {
	    URL url = new URL(FAVOURITES_DIR);
	    InputStreamReader reader = new InputStreamReader(url
							     .openConnection().getInputStream());
	    BufferedReader in = new BufferedReader(reader);
	    String line = in.readLine();
	    while (line != null) {
		if (line.startsWith(".")) {
		    // ignore .htacess etc
		    continue;
		}
		favourites.add(new FavouritesInfo(null, line, null));
		line = in.readLine();
	    }
	    in.close();
	    reader.close();

	    for (FavouritesInfo f: favourites) {
		// TODO checkout the Songtable constructors - messy 
		f.songTable = new SongTable(new Vector<SongInformation>());
		f.songTable.loadTableFromStore(FAVOURITES_DIR +
					   f.owner);

		Favourites fav = new Favourites(songTable, 
						f.songTable, 
						f.owner);
		addTab(f.owner, null, fav, f.owner);
	    }
	} catch(Exception e) {
	    System.out.println(e.toString());
	}

	/*
	Path favouritesPath = FileSystems.getDefault().getPath(FAVOURITES_DIR);
	try {
	    DirectoryStream<Path> stream = 
		Files.newDirectoryStream(favouritesPath);
	    for (Path entry: stream) {
		int nelmts = entry.getNameCount();
		Path last = entry.subpath(nelmts-1, nelmts);
		if (last.toString().startsWith(".")) {
		    // ignore .htaccess etc
		    continue;
		}
		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);
		    }});

	}
    }

    class FavouritesInfo {
	public SongTable songTable;
	public String owner;
	public Image image;
	
	public FavouritesInfo(SongTable songTable,
			      String owner,
			      Image image) {
	    this.songTable = songTable;
	    this.owner = owner;
	    this.image = image;
	}
    }

}

      

and

	
package newmarch.songtable;

import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.SwingUtilities;
import java.util.regex.*;
import java.io.*;
import java.nio.file.FileSystems;
import java.nio.file.*;
import java.net.Socket;
import java.net.InetAddress;
import java.util.Vector;

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

    private JTable table;
    private DefaultTableModel tmodel = new DefaultTableModel();

    // whose favorites 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");
	}


	System.out.println("Favourites for user: " + user);
	
	tmodel.addColumn("ID");
	tmodel.addColumn("Artist");
	tmodel.addColumn("Title");
	tmodel.addColumn("Path");
	
	int n = 0;
	java.util.Iterator<SongInformation> iter = favouriteSongs.iterator();
	while(iter.hasNext()) {
	    SongInformation info = iter.next();
	    if (info == null)
		continue;
	    /*
	    System.out.println("UID for SongInformation " + 
			       ObjectStreamClass.lookup(info.getClass()).getSerialVersionUID());
	    */
	    model.add(n++, info);

	    tmodel.addRow(info.toVector());
	}



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

	table = new JTable(tmodel);
	table.setRowSorter(new TableRowSorter(tmodel));
	// hide column 3
	table.removeColumn(table.getColumnModel().getColumn(3));
	table.setFillsViewportHeight(true);
	table.setFont(font);
	table.getColumnModel().getColumn(0).setPreferredWidth(100);
	table.getColumnModel().getColumn(0).setMaxWidth(100);
	table.getColumnModel().getColumn(1).setPreferredWidth(200);
	table.getColumnModel().getColumn(1).setMaxWidth(200);
	table.setRowHeight(24);

	//JScrollPane scrollPane = new JScrollPane(list);
	JScrollPane scrollPane = new JScrollPane(table);

	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);
		    */

		    int realIndex = table.convertRowIndexToModel(table.getSelectedRow());
		    tmodel.removeRow(realIndex);
		    favouriteSongs.songs.removeElementAt(realIndex);
		    saveToStore();
		}
	    });

	addSong.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent e) {
		    SongInformation song = songTable.getSelection();
		    //model.addElement(song);
		    tmodel.addRow(song.toVector());
		    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();
	    */

	    //favouriteSongs.saveTableToStore("/server/KARAOKE/favourites/" + user);
	    favouriteSongs.saveTableToStore(AllFavourites.FAVOURITES_DIR + user);

	    /*
	    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());
	*/
	String path = table.getModel().getValueAt(
			      table.convertRowIndexToModel(
							   table.getSelectedRow()), 3).toString();
	System.out.println(path);

	String SERVERIP = "192.168.1.110"; 
	int SERVERPORT = 13000;
	PrintWriter out;
	
	try {
	    InetAddress serverAddr = InetAddress.getByName(SERVERIP);
	    Socket socket = new Socket(serverAddr, SERVERPORT);
				 
	    //send the message to the server
	    out = new PrintWriter(
				  new BufferedWriter(
						     new OutputStreamWriter(socket.getOutputStream())), 
				  true);
	  
	    // Avoid println - on Windows it is \r\n
	    //out.print(song.path + "\n");
	    out.print(path + "\n");
	    out.flush();
	    socket.close();
				
	} catch (Exception e) {
	    System.err.println(e.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;
	}
    }
}

      

The audio player side

On the audio player, a service has to listen for URLs to be played. Each song will be pulled off the file server by an HTTP request (using a tool such as wget). This should maintain a queue of requests, playing the next song as the previous one completes. This should show the queue, so is implemented using a Java program showing a Swing list.

The program is Player.java:

	
import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import javax.swing.*;
import java.io.*;
import java.net.*;


public class Player extends JFrame {

    private DefaultListModel model = new DefaultListModel();
    private JList queue;
    private JLabel playingLabel;

    public boolean isPlaying = false;
    private Process songPlayingProcess = null;

    // we have to keep track of the tempo rate, vlc won't tell us
    private double rate = 1.0;


    public static void main(String[] args) {
	new Player();
    }

    public Player() {
	Container contentPane = getContentPane();

	setTitle("Song Queue");
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        setSize(400, 400);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

	contentPane.setLayout(new BorderLayout());

	queue = new JList(model);
	contentPane.add(queue, BorderLayout.CENTER);

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

	playingLabel = new JLabel(" ");
	bottomPanel.add(playingLabel);

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

	JButton stopBtn = new JButton("Stop");
	JButton fasterBtn = new JButton("Faster");
	JButton slowerBtn = new JButton("Slower");

	controlPanel.add(stopBtn);
	controlPanel.add(fasterBtn);
	controlPanel.add(slowerBtn);

	stopBtn.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent evt){
		    if (songPlayingProcess != null) {
			PrintStream writer = new PrintStream(songPlayingProcess.getOutputStream());
			writer.println("quit");
			writer.flush();
		    }
		}
	    });


	fasterBtn.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent evt){
		    if (songPlayingProcess != null) {
			PrintStream writer = new PrintStream(songPlayingProcess.getOutputStream());
			rate += 0.03;
			writer.println("rate " + rate);
			writer.flush();
		    }
		}
	    });

	slowerBtn.addActionListener(new ActionListener(){
                public void actionPerformed(ActionEvent evt){
		    if (songPlayingProcess != null) {
			PrintStream writer = new PrintStream(songPlayingProcess.getOutputStream());
			rate -= 0.03;
			writer.println("rate " + rate);
			writer.flush();
		    }
		}
	    });

	/*
	model.addListDataListener(new ListDataListener() {


	    });
	*/
	setVisible(true);

	new Thread(new TCPListener()).start();
    }

    public void playSong(SongInformation info) {
	isPlaying = true;
	try {
	    if (info.path.endsWith(".zip")) {
		songPlayingProcess = Runtime.getRuntime().exec(new String[] {"bash", "playZip", info.path});
	    }  else if (info.path.endsWith(".kar")) {
		songPlayingProcess = Runtime.getRuntime().exec(new String[] {"bash", "playKar", info.path});
	    } else {
		isPlaying = false;
		return;
	    }
	    playingLabel.setText("Now playing: " + info.toString());
	    new Thread(new WaitForSongToEnd()).start();
	} catch(IOException e) {
	    System.err.println(e.toString());
	    isPlaying = false;
	}
    }

    public void playNextSong() {
	if (model.isEmpty()) {
	    return;
	}
	SongInformation info = (SongInformation) model.remove(0);
	playSong(info);
    }

    class SongInformationRenderer extends JLabel implements ListCellRenderer {

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

    class WaitForSongToEnd implements Runnable {

	public void run() {
	    try {
		songPlayingProcess.waitFor();
	    } catch(Exception e) {
		System.err.println(e.toString());
	    }
	    Player.this.isPlaying = false;
	    rate = 1.0;
	    songPlayingProcess = null;
	    System.out.println("Song finished!");
	    Player.this.playingLabel.setText(" ");
	    Player.this.playNextSong();
	}
    }

    class TCPListener implements Runnable {
	public int PORT = 13000;

	public void run() {
	    try {
		System.out.println("Listening...");
		ServerSocket s = new ServerSocket(PORT);
		while (true) {
		    Socket incoming = s.accept();
		    handleSocket(incoming);
		    incoming.close();
		}
	    } catch(IOException e) {
		System.err.println(e.toString());
	    }
	}

	public void handleSocket(Socket incoming) {
	    try {
		BufferedReader reader =
		    new BufferedReader(new InputStreamReader(
							 incoming.getInputStream()));

		String fname = reader.readLine();
		if (fname == null) {
		    return;
		}
		System.out.println("Echo: " + fname);
		
		if (fname.endsWith(".zip") || 
		    fname.endsWith(".mp3") || 
		    fname.endsWith(".kar")) {
		    // lose extension
		    String root = fname.substring(0, fname.length()-4);
		    // lose /.../
		    root = root.substring(root.lastIndexOf('/')+1);
		    //System.err.println(" root " + root);
		    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(fname,
							       index,
							       title,
							       artist);
		    if (isPlaying) {
			model.addElement(info);
		    } else {
			Player.this.playSong(info);
		    }
		//songs.add(info);
	    }


	    } catch(IOException e) {
		System.err.println(e.toString());
	    }
	}
    }
}
      

It contains buttons to control the speed of play and to stop play. More importantly, it hands control to different shell scripts based on the type of file downloaded.

Jack or PulseAudio?

One of the common requests is to raise or lower the pitch of a song. Jack can easily build a network of components, and if one of these is jack-rack then it can use a LADPSA plugin, such as the TAP pitch plugin.

So I use Jack.

Playing an MP3+G file

An Mp3+G pair is pulled off the server as a ZIP file. After unzipping, it can be played by vlc. vlc can use a Lua interface to which you can send commands such as "faster", "slower".

The script is playZip

	#!/bin/bash

VLC_OPTS=" -I luaintf --lua-intf cli   --play-and-exit --aout jack"

HTTP=http:/

[ ! -d /tmp/karaoke ] && mkdir /tmp/karaoke
rm -f /tmp/karaoke/*
wget "${HTTP}$*" -O /tmp/karaoke/tmp.zip
unzip -d /tmp/karaoke /tmp/karaoke/tmp.zip
rm /tmp/karaoke/tmp.zip
vlc $VLC_OPTS /tmp/karaoke/*.??3

      

Playing a KAR file

KAR files ripped of the Sonken player may have audio plus lyrics, or lyrics only with the audio in a linked WMA files. The MIDI player timidity can be used to play the KAR part, but for WMA files another player such as mplayer is needed.

An annoying part of timidity is that you cannot set the Jack output device to connect to. So you need to wait till it has registered with Jack, and then call jack-connect to link it to jack-rack.

The shell script is playKar:

	#!/bin/bash
set -x

HTTP=http:/

echo Fetching $*

[ ! -d /tmp/karaoke ] && mkdir /tmp/karaoke
rm -f /tmp/karaoke/*
wget "${HTTP}$*" -O /tmp/karaoke/tmp.kar

WMAFILE="${*/.kar/.wma}"
echo wget "${HTTP}${WMAFILE}" -O /tmp/karaoke/tmp.wma > /tmp/out0
wget "${HTTP}${WMAFILE}" -O /tmp/karaoke/tmp.wma

if [ -s "/tmp/karaoke/tmp.wma" ]
then
    bash ./playMplayer /tmp/karaoke/tmp.wma &
    cd ../timidity
    ./TiMidity++-2.14.0/timidity/timidity -d. -ix --trace --trace-text-meta /tmp/karaoke/tmp.kar &
    cd ../Karaoke
    sleep 3
    jack_disconnect TiMidity:port_1 system:playback_1
    jack_disconnect TiMidity:port_2 system:playback_2

    wait %1
    killall mplayer
    # WMA file
#    java -jar WMAPlayer.jar /tmp/karaoke/tmp.kar  > /dev/null 2> /dev/null
else
    # pykar /tmp/karaoke/tmp.kar > /dev/null 2> /dev/null
    # give pykar time to give up audio card
    #sleep 4

    cd ../timidity
    ./TiMidity++-2.14.0/timidity/timidity -d. -ix --trace --trace-text-meta /tmp/karaoke/tmp.kar &
    sleep 3
    jack_disconnect TiMidity:port_1 system:playback_1
    jack_disconnect TiMidity:port_2 system:playback_2
    jack_connect TiMidity:port_1 jack_rack:in_1
    jack_connect TiMidity:port_2 jack_rack:in_2
fi
    wait %1
    cd ../Karaoke
fi

      

TiMidity plugin

The last bit of the puzzle is how TiMidity will show the lyrics. There is no supplied plugin that will display lyrics in the right format. The flags --trace --trace-meta-text will make the lyrics available in real-time to a plugin. I wrote one based on Xlib, with drawing done by Cairo and Pango. It is x_code.c

	
#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include <gtk/gtk.h>

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>

#include "mytimidity.h"

#define WIDTH 720
#define HEIGHT 480

#define NUM_LINES 4

Display *display;
Window window;
GC gc, color_gc;

struct _lyric_t {
    gchar *lyric;
    long tick;

};
typedef struct _lyric_t lyric_t;

struct _lyric_lines_t {
    char *language;
    char *title;
    char *performer;
    GArray *lines; // array of GString *
};
typedef struct _lyric_lines_t lyric_lines_t;

GArray *lyrics;
GString *lyrics_array[NUM_LINES];

lyric_lines_t lyric_lines;

typedef struct _coloured_line_t {
    gchar *line;
    gchar *front_of_line;
    gchar *marked_up_line;
    PangoAttrList *attrs;
} coloured_line_t;

int height_lyric_pixbufs[] = {100, 200, 300, 400}; // vertical offset of lyric in video
int coloured_text_offset;

// fluid_player_t* player;

// int current_panel = 1;  // panel showing current lyric line
int current_line = 0;  // which line is the current lyric
gchar *current_lyric;   // currently playing lyric line
GString *front_of_lyric;  // part of lyric to be coloured red
//GString *end_of_lyric;    // part of lyrci to not be coloured


gchar *markup[] = {"<span font=\"28\" foreground=\"RED\">",
		   "</span><span font=\"28\" foreground=\"white\">",
		   "</span>"};
gchar *markup_newline[] = {"<span foreground=\"black\">",
			   "</span>"};
GString *marked_up_label;

PangoFontDescription *font_description;

cairo_surface_t *surface;
cairo_t *cr;

extern ControlMode  *ctl;

ControlMode video_ctl=
    {
	"x interface", 'x',
	"x",
	1,          /* verbosity */
	1,          /* trace playing */
	0,          /* opened */
	0,          /* flags */
	ctl_open,
	ctl_close,
	pass_playing_list,
	ctl_read,
	NULL,       /* write */
	cmsg,
	ctl_event
    };

static FILE *outfp;
int video_error_count;
static char *current_file;
struct midi_file_info *current_file_info;

static int pass_playing_list(int number_of_files, char *list_of_files[]) {
    int n;

    for (n = 0; n < number_of_files; n++) {
	printf("Playing list %s\n", list_of_files[n]);
	
	current_file = list_of_files[n];
	play_midi_file( list_of_files[n]);
    }
    XCloseDisplay(display);
    exit(0);
    return 0;
}

static void paint_background() {
    cr = cairo_create(surface);
    cairo_set_source_rgb(cr, 0.0, 0.8, 0.0);
    cairo_paint(cr);
    cairo_destroy(cr);
}

static void set_font() {
    font_description = pango_font_description_new ();
    pango_font_description_set_family (font_description, "serif");
    pango_font_description_set_weight (font_description, PANGO_WEIGHT_BOLD);
    pango_font_description_set_absolute_size (font_description, 32 * PANGO_SCALE);
}

static int draw_text(char *text, float red, float green, float blue, int height, int offset) {
  // See http://cairographics.org/FAQ/
  PangoLayout *layout;
  int width, ht;
  cairo_text_extents_t extents;

  layout = pango_cairo_create_layout (cr);
  pango_layout_set_font_description (layout, font_description);
  pango_layout_set_text (layout, text, -1);

  if (offset == 0) {
      pango_layout_get_size(layout, &width, &ht);
      offset = (WIDTH - (width/PANGO_SCALE)) / 2;
  }

  cairo_set_source_rgb (cr, red, green, blue);
  cairo_move_to (cr, offset, height);
  pango_cairo_show_layout (cr, layout);

  g_object_unref (layout);
  return offset;
}

static void init_X() {
    int screen;
    unsigned long foreground, background;
    XSizeHints hints;
    char **argv = NULL;
    XGCValues gcValues;
    Colormap colormap;
    XColor rgb_color, hw_color;
    Font font;
    //char *FNAME = "hanzigb24st";
    char *FNAME = "-misc-fixed-medium-r-normal--0-0-100-100-c-0-iso10646-1";

    display = XOpenDisplay(NULL);
    if (display == NULL) {
	fprintf(stderr, "Can't open dsplay\n");
	exit(1);
    }
    screen = DefaultScreen(display);
    foreground = BlackPixel(display, screen);
    background = WhitePixel(display, screen);

    window = XCreateSimpleWindow(display,
				 DefaultRootWindow(display),
				 0, 0, WIDTH, HEIGHT, 10,
				 foreground, background);
    hints.x = 0;
    hints.y = 0;
    hints.width = WIDTH;
    hints.height = HEIGHT;
    hints.flags = PPosition | PSize;

    XSetStandardProperties(display, window, 
			   "X", "X", 
			   None,
			   argv, 0,
			   &hints);

    XMapWindow(display, window);


    set_font();
    surface = cairo_xlib_surface_create(display, window,
					DefaultVisual(display, 0), WIDTH, HEIGHT);
    cairo_xlib_surface_set_size(surface, WIDTH, HEIGHT);

    paint_background();

    /*
    cr = cairo_create(surface);
    draw_text(g_array_index(lyric_lines.lines, GString *, 0)->str,
	      0.0, 0.0, 1.0, height_lyric_pixbufs[0]);
    draw_text(g_array_index(lyric_lines.lines, GString*, 1)->str,
	      0.0, 0.0, 1.0, height_lyric_pixbufs[0]);
    cairo_destroy(cr);
    */
    XFlush(display);
}


static int inited_video = 0;
/*ARGSUSED*/
static int ctl_open(int using_stdin, int using_stdout)
{
    init_X();

    // dont know what this function does
    /*
      if (current_file != NULL) {
      current_file_info = get_midi_file_info(current_file, 1);
      printf("Opening info for %s\n", current_file);
      } else {
      printf("Current is NULL\n");
      }
    */
    ctl->opened = 1;
    return 0;
}

static void ctl_close(void)
{
    fflush(outfp);
    video_ctl.opened=0;
    exit(0);
}

/*ARGSUSED*/
static int ctl_read(int32 *valp)
{
    return RC_NONE;
}

static int cmsg(int type, int verbosity_level, char *fmt, ...)
{
    /*
      va_list ap;

      if ((type==CMSG_TEXT || type==CMSG_INFO || type==CMSG_WARNING) &&
      video_ctl.verbosity<verbosity_level)
      return 0;
      va_start(ap, fmt);
      if(type == CMSG_WARNING || type == CMSG_ERROR || type == CMSG_FATAL)
      video_error_count++;
      if (!video_ctl.opened)
      {
      vfprintf(stderr, fmt, ap);
      fputs(NLS, stderr);
      }
      else
      {
      vfprintf(outfp, fmt, ap);
      fputs(NLS, outfp);
      fflush(outfp);
      }
      va_end(ap);
    */
    return 0;
}

static void ctl_total_time(long tt)
{
    /*
      int mins, secs;
      if (video_ctl.trace_playing)
      {
      secs=(int)(tt/play_mode->rate);
      mins=secs/60;
      secs-=mins*60;
      cmsg(CMSG_INFO, VERB_NORMAL,
      "Total playing time: %3d min %02d s", mins, secs);
      }
    */
}

static void ctl_file_name(char *name)
{
    current_file = name;

    if (video_ctl.verbosity>=0 || video_ctl.trace_playing)
	cmsg(CMSG_INFO, VERB_NORMAL, "Playing %s", name);
}

static void ctl_current_time(int secs)
{
    int mins;
    static int prev_secs = -1;

#ifdef __W32__
    if(wrdt->id == 'w')
	return;
#endif /* __W32__ */
    if (ctl->trace_playing && secs != prev_secs)
	{
	    prev_secs = secs;
	    mins=secs/60;
	    secs-=mins*60;
	    fprintf(stdout, "\r%3d:%02d", mins, secs);
	}
}

void build_lyric_lines() {
    int n;
    lyric_t *plyric;
    GString *line = g_string_new("");
    GArray *lines =  g_array_sized_new(FALSE, FALSE, sizeof(GString *), 64);

    lyric_lines.title = NULL;

    n = 1;
    char *evt_str;
    while ((evt_str = event2string(n++)) != NULL) {

        gchar *lyric = evt_str+1;
	printf("Building line %s\n", lyric);

	if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'L')) {
	    lyric_lines.language =  lyric + 2;
	    continue;
	}

	if ((strlen(lyric) >= 2) && (lyric[0] == '@') && (lyric[1] == 'T')) {
	    if (lyric_lines.title == NULL) {
		lyric_lines.title = lyric + 2;
	    } else {
		lyric_lines.performer = lyric + 2;
	    }
	    continue;
	}

	if (lyric[0] == '@') {
	    // some other stuff like @KMIDI KARAOKE FILE
	    continue;
	}

	if ((lyric[0] == '/') || (lyric[0] == '\\')) {
	    // start of a new line
	    // add to lines
	    g_array_append_val(lines, line);
	    line = g_string_new(lyric + 1);
	}  else {
	    line = g_string_append(line, lyric);
	}
    }
    lyric_lines.lines = lines;
    
    printf("Title is %s, performer is %s, language is %s\n", 
	   lyric_lines.title, lyric_lines.performer, lyric_lines.language);
    for (n = 0; n < lines->len; n++) {
	printf("Line is %s\n", g_array_index(lines, GString *, n)->str);
    }
    
}

static void ctl_lyric(int lyricid)
{
    char *lyric;

    current_file_info = get_midi_file_info(current_file, 1);

    lyric = event2string(lyricid);
    if(lyric != NULL)
	lyric++;
    printf("Got a lyric %s\n", lyric);


    if (*lyric == '\\') {
	// int next_panel = (current_panel+2) % NUM_LINES; // really (current_panel+2)%2
	int next_line = current_line + NUM_LINES;
	gchar *next_lyric;

	if (current_line + NUM_LINES < lyric_lines.lines->len) {
	    current_line += 1;
	    
	    //lyrics_array[(next_line-1) % NUM_LINES] = 
	    //	g_array_index(lyric_lines.lines, GString *, next_line);
	    
	    // update label for next line after this one
	    next_lyric = g_array_index(lyric_lines.lines, GString *, next_line)->str;

	} else {
	    current_line += 1;
	    lyrics_array[(next_line-1) % NUM_LINES] = NULL;
	    next_lyric = "";
	}

	// set up new line as current line
	if (current_line < lyric_lines.lines->len) {
	    GString *gstr = g_array_index(lyric_lines.lines, GString *, current_line);
	    current_lyric = gstr->str;
	    front_of_lyric = g_string_new(lyric+1); // lose	slosh
	}	  
	printf("New line. Setting front to %s end to \"%s\"\n", lyric+1, current_lyric); 


	// Now draw stuff
	paint_background();

	cr = cairo_create(surface);

	int n;
	for (n = 0; n < NUM_LINES; n++) {
	    //lyrics_array[n] = g_array_index(lyric_lines.lines, GString *, n+1);
	    if (lyrics_array[n] != NULL) {
		draw_text(lyrics_array[n]->str,
			  0.0, 0.0, 0.5, height_lyric_pixbufs[n], 0);
	    }
	}
	// redraw current and next lines
	if (current_line < lyric_lines.lines->len) {
	    if (current_line >= 2) {
		// redraw last line still in red
		GString *gstr = lyrics_array[(current_line-2) % NUM_LINES];
		if (gstr != NULL) {
		    draw_text(gstr->str,
			      1.0, 0.0, 00, 
			      height_lyric_pixbufs[(current_line-2) % NUM_LINES],
			      0);
		}
	    }
	    // draw next line in brighter blue
	    coloured_text_offset = draw_text(lyrics_array[(current_line-1) % NUM_LINES]->str,
		      0.0, 0.0, 1.0, height_lyric_pixbufs[(current_line-1) % NUM_LINES], 0);
	    printf("coloured text offset %d\n", coloured_text_offset);
	}

	//try
	if (next_line < lyric_lines.lines->len) {
	    lyrics_array[(next_line-1) % NUM_LINES] = 
		g_array_index(lyric_lines.lines, GString *, next_line);
	}
	

	    /*
	draw_text(current_lyric, 0.0, 0.0, 1.0, 
		  height_lyric_pixbufs[(current_line-1) % NUM_LINES]);


	draw_text(next_lyric, 0.0, 0.0, 1.0, 
		  height_lyric_pixbufs[(next_line-1) % NUM_LINES]);
	    */
	cairo_destroy(cr);
	XFlush(display);


    } else {
	// change text colour as chars are played
	if ((front_of_lyric != NULL) && (lyric != NULL)) {
	    g_string_append(front_of_lyric, lyric);
	    char *s = front_of_lyric->str;
	    //coloured_lines[current_panel].front_of_line = s;

	    cairo_t *cr = cairo_create(surface);

	    // See http://cairographics.org/FAQ/
	    draw_text(s, 1.0, 0.0, 0.0, 
		      height_lyric_pixbufs[(current_line-1) % NUM_LINES], 
		      coloured_text_offset);

	    cairo_destroy(cr);
	    XFlush(display);

	}
    }
}


static void ctl_event(CtlEvent *e)
{
    //printf("Got ctl event %d\n", e->type);
    switch(e->type)
	{
	case CTLE_NOW_LOADING:
	    ctl_file_name((char *)e->v1);
	    break;
	case CTLE_LOADING_DONE:
	    // MIDI file is loaded, about to play
	    current_file_info = get_midi_file_info(current_file, 1);
	    if (current_file_info != NULL) {
		printf("file info not NULL\n");
	    } else {
		printf("File info is NULL\n");
	    }

	    int n = 1;
	    char *evt_str;
	    while ((evt_str = event2string(n++)) != NULL) {
		printf("Event in tabel: %s\n", evt_str);
	    }

	    build_lyric_lines();
	    cr = cairo_create(surface);

	    // draw line to be sung slightly brighter
	    // than the rest
	    for (n = 0; n < NUM_LINES; n++) {
		lyrics_array[n] = g_array_index(lyric_lines.lines, GString *, n+1);
		draw_text(lyrics_array[n]->str,
			  0.0, 0.0, 0.5, height_lyric_pixbufs[n], 0);
	    }
	    draw_text(lyrics_array[0]->str,
		      0.0, 0.0, 1.0, height_lyric_pixbufs[0], 0);

	    /*
	    draw_text(g_array_index(lyric_lines.lines, GString *, 1)->str,
		      0.0, 0.0, 1.0, height_lyric_pixbufs[0]);
	    draw_text(g_array_index(lyric_lines.lines, GString*, 2)->str,
		      0.0, 0.0, 1.0, height_lyric_pixbufs[1]);
	    draw_text(g_array_index(lyric_lines.lines, GString*, 3)->str,
		      0.0, 0.0, 1.0, height_lyric_pixbufs[2]);
	    */
	    cairo_destroy(cr);
	    XFlush(display);
	    
	    break;
	case CTLE_PLAY_START:

	    ctl_total_time(e->v1);
	    break;
        case CTLE_PLAY_END:
	    printf("Exiting, play ended\n");
	    exit(0);
	    break;
	case CTLE_CURRENT_TIME:
	    ctl_current_time((int)e->v1);
	    break;
	case CTLE_LYRIC:
	    ctl_lyric((int)e->v1);
	    break;
	    /*
	      case CTLE_REFRESH:
	      printf("Refresh\n");
	      break;
	    */
	default:
	    0;
	    //printf("Other event\n");
	}
}

/*
 * interface_<id>_loader();
 */
ControlMode *interface_x_loader(void)
{
    return &video_ctl;
}

      

Conclusion

All done! Well, as with anything, more can be done. But more will, I think, require use of the GPU in these small computers. Plus X is giving way to Wayland (or Mir on Ubuntu) and that should have hooks into the GPUs. So it's off to there I will probably go next.

Regards, and good luck with your own projects!


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