View Javadoc

1   package net.sf.jack4j.examples;
2   
3   /*
4   Copyright (C) 2008 Ondrej Par
5   
6   This program is free software; you can redistribute it and/or modify
7   it under the terms of the GNU Lesser General Public License as published by
8   the Free Software Foundation; either version 2.1 of the License, or
9   (at your option) any later version.
10  
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  GNU Lesser General Public License for more details.
15  
16  You should have received a copy of the GNU Lesser General Public License
17  along with this program; if not, write to the Free Software
18  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19  
20  */
21  
22  import java.io.IOException;
23  import java.nio.ByteBuffer;
24  import java.util.EnumSet;
25  
26  import net.sf.jack4j.AbstractJackClient;
27  import net.sf.jack4j.JackException;
28  import net.sf.jack4j.JackLocalMidiPort;
29  import net.sf.jack4j.JackLocalPort;
30  import net.sf.jack4j.JackMidiEvent;
31  import net.sf.jack4j.JackMidiPortBuffer;
32  import net.sf.jack4j.JackPortFlag;
33  
34  /**
35   * Very simple MIDI synthesizer.
36   * 
37   * <p>
38   * See comments in the code that describe the usage of Jack4j library.
39   * 
40   * <p>
41   * To run the examples, you need to have the native Jack4j library in your
42   * system load path (LD_LIBRARY_PATH under Linux).
43   * 
44   * <p>
45   * This example client expects single command line parameter - client name.
46   * 
47   * @author repa
48   */
49  public class MidiSine extends AbstractJackClient {
50  
51  	/**
52  	 * Pre-computed frequencies (Hz) of MIDI notes #0-#127.
53  	 */
54  	private static float[] midiNoteFreq;
55  	static {
56  		midiNoteFreq = new float[128];
57  		for (int i = 0; i < 127; i++) {
58  			midiNoteFreq[i] = (float) ((440.0f / 32) * Math.pow(2, (i - 9) / 12.0f));
59  		}
60  	}
61  
62  	private JackLocalMidiPort midiIn;
63  	private JackLocalPort audioOut;
64  
65  	private float currentPhase;
66  	private int currentNote;
67  	private float currentVolume;
68  
69  	/**
70  	 * This is callback method, invoked by Jack client thread during each
71  	 * process cycle.
72  	 * 
73  	 * <p>
74  	 * This method receives MIDI events and produces the sound.
75  	 * 
76  	 * <p>
77  	 * The method is expected to put <code>bufferSize</code> samples into
78  	 * <i>buffer</i> associated with the <i>output audio port</i>.
79  	 * 
80  	 * <p>
81  	 * The method also reads MIDI events from the buffer associated with the
82  	 * <i>input MIDI port</i>,
83  	 * 
84  	 * <p>
85  	 * Ports were created earlier, during client initialization.
86  	 */
87  	@Override
88  	public int process(int bufferSize) throws Exception {
89  		// obtain ByteBuffer associated with the output audio port
90  		ByteBuffer outBuffer = audioOut.getByteBuffer(bufferSize);
91  		// ALWAYS clear the buffer before use! Otherwise, limit and position would be undefined:
92  		outBuffer.clear();
93  
94  		// the MIDI buffers are handled in a different way than audio buffers: 
95  		JackMidiPortBuffer inBuffer = midiIn.initializeMidiBuffer(bufferSize);
96  
97  		// There may be any number of events in the buffer. They may be retrieved by index.
98  		// Each event "happens" on a certain time during current process cycle.
99  		// The "time" property of the event has a value between 0 (happens at the beginning
100 		// of the current process cycle) and bufferSize (happens at the very end of the current
101 		// process cycle.
102 		int nextEventIndex = 0;
103 		JackMidiEvent nextEvent = (inBuffer.getEventCount() > 0) ? inBuffer.getEvent(0) : null;
104 
105 		int sampleRate = getSampleRate();
106 
107 		// Simultaneously process MIDI events and produce sound.
108 		// Iterate through all sample points in the current process cycle.
109 		for (int i = 0; i < bufferSize; i++) {
110 			while ((nextEvent != null) && (nextEvent.getTime() == i)) {
111 				// The MIDI event occurs now; process it.
112 
113 				// The returned byte array will contain the actual MIDI octets.
114 				byte[] data = nextEvent.getData();
115 
116 				// Test the first octet (MIDI command):
117 				if (data[0] == (byte) 0x80) { // NOTE OFF
118 					// The second octet contains MIDI note number.
119 					// If it's the note we're currently playing, stop playing it.
120 					if (currentNote == data[1]) {
121 						System.out.println("Switching note OFF");
122 						currentNote = -1;
123 					}
124 				} else if (data[0] == (byte) 0x90) { // NOTE ON
125 					// The second octet contains MIDI note number,
126 					// and the third octet contains velocity (volume).
127 					currentNote = data[1];
128 					currentPhase = 0.0f;
129 					currentVolume = ((float) data[2]) / 0x7f;
130 					System.out.println("Playing note " + currentNote + " at volume " + currentVolume);
131 				}
132 
133 				// fetch the following event
134 				nextEventIndex++;
135 				nextEvent = (nextEventIndex < inBuffer.getEventCount()) ? inBuffer.getEvent(nextEventIndex) : null;
136 			}
137 
138 			// Now, let's calculate the next audio sample.
139 			float sample;
140 
141 			if (currentNote < 0) {
142 				// produce silence
143 				sample = 0.0f;
144 			} else {
145 				// produce sound at correct frequency
146 				sample = (float) (currentVolume * Math.sin(2 * Math.PI * currentPhase));
147 
148 				// increase the phase for the next sample
149 				currentPhase += midiNoteFreq[currentNote] / sampleRate;
150 				if (currentPhase > 1.0) {
151 					currentPhase -= 1.0;
152 				}
153 			}
154 
155 			// Put the computed sample into output buffer.
156 			outBuffer.putFloat(sample);
157 		}
158 
159 		return 0;
160 	}
161 
162 	public MidiSine(String clientName, boolean useExactName, boolean canStartServer, String serverName)
163 	        throws JackException {
164 		// Call to super constructor will register us with Jack server
165 		super(clientName, useExactName, canStartServer, serverName);
166 
167 		// Setting default ThreadInit callback is necessary, 
168 		// otherwise other callbacks won't work properly (the Jack client thread
169 		// must be registered with JVM first)
170 		setDefaultThreadInitCallback();
171 
172 		// Set the native callback that will invoke our process(int) method.
173 		setDefaultProcessCallback();
174 
175 		// Add input MIDI port.
176 		midiIn = addMidiPort("in", EnumSet.of(JackPortFlag.IS_INPUT));
177 
178 		// Add output audio port.
179 		audioOut = addAudioPort("out", EnumSet.of(JackPortFlag.IS_OUTPUT));
180 
181 		currentNote = -1;
182 		currentPhase = 0.0f;
183 
184 		// we are not activated yet, thus the callbacks won't be invoked
185 	}
186 
187 	/**
188 	 * Activation and main event loop.
189 	 */
190 	public void run() throws JackException {
191 		// Activate the client; until now, callbacks weren't invoked
192 		activate();
193 
194 		System.out.println("CONNECT BOTH PORTS created by the client");
195 		System.out.println("You can connect the output audio port to the soundcard to hear the sound,");
196 		System.out.println("and connect MIDI keyboard (vkeybd, for example) to the input MIDI port.");
197 
198 		// wait for user input; the Jack thread runs simultaneously
199 		System.out.println("Press any key to quit.");
200 		try {
201 			System.in.read();
202 		} catch (IOException e) {
203 			e.printStackTrace();
204 		}
205 
206 		// close the client (disconnect from Jack)
207 		close();
208 	}
209 
210 	public static void main(String[] args) throws Exception {
211 		if (args.length != 1) {
212 			throw new IllegalArgumentException("Expecting single parameter (client name)");
213 		}
214 		String clientName = args[0];
215 
216 		MidiSine midiSine = new MidiSine(clientName, false, true, null);
217 
218 		midiSine.run();
219 	}
220 }