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 }