Added gain control and normalization, along with peak display

This commit is contained in:
2018-11-13 13:26:54 +00:00
parent 024626019d
commit 14e6709c29
6 changed files with 249 additions and 61 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 473 B

View File

@@ -75,12 +75,14 @@ public class AudiobookRecorder extends JFrame {
JScrollBar sampleScroll; JScrollBar sampleScroll;
JSpinner postSentenceGap; JSpinner postSentenceGap;
JSpinner gainPercent;
JCheckBox locked; JCheckBox locked;
JCheckBox attention; JCheckBox attention;
JCheckBox ethereal; JCheckBox ethereal;
JButtonSpacePlay reprocessAudioFFT; JButtonSpacePlay reprocessAudioFFT;
JButtonSpacePlay reprocessAudioPeak; JButtonSpacePlay reprocessAudioPeak;
JButtonSpacePlay normalizeAudio;
Thread playingThread = null; Thread playingThread = null;
@@ -363,6 +365,7 @@ public class AudiobookRecorder extends JFrame {
sampleWaveform.setMarkers(selectedSentence.getStartOffset(), selectedSentence.getEndOffset()); sampleWaveform.setMarkers(selectedSentence.getStartOffset(), selectedSentence.getEndOffset());
sampleWaveform.setAltMarkers(selectedSentence.getStartCrossing(), selectedSentence.getEndCrossing()); sampleWaveform.setAltMarkers(selectedSentence.getStartCrossing(), selectedSentence.getEndCrossing());
postSentenceGap.setValue(selectedSentence.getPostGap()); postSentenceGap.setValue(selectedSentence.getPostGap());
gainPercent.setValue((int)(selectedSentence.getGain() * 100d));
} }
} }
}); });
@@ -375,12 +378,23 @@ public class AudiobookRecorder extends JFrame {
sampleWaveform.setMarkers(selectedSentence.getStartOffset(), selectedSentence.getEndOffset()); sampleWaveform.setMarkers(selectedSentence.getStartOffset(), selectedSentence.getEndOffset());
sampleWaveform.setAltMarkers(selectedSentence.getStartCrossing(), selectedSentence.getEndCrossing()); sampleWaveform.setAltMarkers(selectedSentence.getStartCrossing(), selectedSentence.getEndCrossing());
postSentenceGap.setValue(selectedSentence.getPostGap()); postSentenceGap.setValue(selectedSentence.getPostGap());
gainPercent.setValue((int)(selectedSentence.getGain() * 100d));
} }
} }
}); });
normalizeAudio = new JButtonSpacePlay(Icons.normalize, "Normalize audio", new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (selectedSentence != null) {
selectedSentence.normalize();
sampleWaveform.setData(selectedSentence.getAudioData());
}
}
});
postSentenceGap = new JSpinner(new SteppedNumericSpinnerModel(0, 5000, 100, 0)); postSentenceGap = new JSpinner(new SteppedNumericSpinnerModel(0, 5000, 100, 0));
postSentenceGap.setPreferredSize(new Dimension(75, 20)); postSentenceGap.setPreferredSize(new Dimension(50, 20));
postSentenceGap.addChangeListener(new ChangeListener() { postSentenceGap.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) { public void stateChanged(ChangeEvent e) {
@@ -391,6 +405,18 @@ public class AudiobookRecorder extends JFrame {
} }
}); });
gainPercent = new JSpinner(new SteppedNumericSpinnerModel(0, 500, 1, 100));
gainPercent.setPreferredSize(new Dimension(50, 20));
gainPercent.addChangeListener(new ChangeListener() {
public void stateChanged(ChangeEvent e) {
JSpinner ob = (JSpinner)e.getSource();
if (selectedSentence != null) {
selectedSentence.setGain((Integer)ob.getValue() / 100d);
sampleWaveform.setData(selectedSentence.getAudioData());
}
}
});
JToolBar controlsTop = new JToolBar(JToolBar.HORIZONTAL); JToolBar controlsTop = new JToolBar(JToolBar.HORIZONTAL);
JToolBar controlsLeft = new JToolBar(JToolBar.VERTICAL); JToolBar controlsLeft = new JToolBar(JToolBar.VERTICAL);
@@ -402,6 +428,7 @@ public class AudiobookRecorder extends JFrame {
controlsLeft.add(reprocessAudioFFT); controlsLeft.add(reprocessAudioFFT);
controlsLeft.add(reprocessAudioPeak); controlsLeft.add(reprocessAudioPeak);
controlsLeft.add(normalizeAudio);
locked = new JCheckBox("Phrase locked"); locked = new JCheckBox("Phrase locked");
locked.setFocusable(false); locked.setFocusable(false);
@@ -469,6 +496,11 @@ public class AudiobookRecorder extends JFrame {
controlsTop.add(new JLabel("Post gap:")); controlsTop.add(new JLabel("Post gap:"));
controlsTop.add(postSentenceGap); controlsTop.add(postSentenceGap);
controlsTop.add(new JLabel("ms")); controlsTop.add(new JLabel("ms"));
controlsTop.add(new JLabel("Gain:"));
controlsTop.add(gainPercent);
controlsTop.add(new JLabel("%"));
controlsTop.add(Box.createHorizontalGlue()); controlsTop.add(Box.createHorizontalGlue());
@@ -1013,7 +1045,19 @@ public class AudiobookRecorder extends JFrame {
} }
}); });
JMenuObject normalizeAll = new JMenuObject("Normalize chapter", c, new ActionListener() {
public void actionPerformed(ActionEvent e) {
JMenuObject o = (JMenuObject)e.getSource();
Chapter c = (Chapter)o.getObject();
for (Enumeration s = c.children(); s.hasMoreElements();) {
Sentence snt = (Sentence)s.nextElement();
snt.normalize();
}
}
});
menu.add(convertAll); menu.add(convertAll);
menu.add(normalizeAll);
menu.addSeparator(); menu.addSeparator();
menu.add(moveUp); menu.add(moveUp);
menu.add(moveDown); menu.add(moveDown);
@@ -1392,6 +1436,7 @@ public class AudiobookRecorder extends JFrame {
prefs.setProperty(String.format("%s.sentence.%08d.locked", keybase, i), snt.isLocked() ? "true" : "false"); prefs.setProperty(String.format("%s.sentence.%08d.locked", keybase, i), snt.isLocked() ? "true" : "false");
prefs.setProperty(String.format("%s.sentence.%08d.attention", keybase, i), snt.getAttentionFlag() ? "true" : "false"); prefs.setProperty(String.format("%s.sentence.%08d.attention", keybase, i), snt.getAttentionFlag() ? "true" : "false");
prefs.setProperty(String.format("%s.sentence.%08d.ethereal", keybase, i), snt.getEthereal() ? "true" : "false"); prefs.setProperty(String.format("%s.sentence.%08d.ethereal", keybase, i), snt.getEthereal() ? "true" : "false");
prefs.setProperty(String.format("%s.sentence.%08d.gain", keybase, i), String.format("%.8f", snt.getGain()));
i++; i++;
} }
} }
@@ -1499,6 +1544,7 @@ public class AudiobookRecorder extends JFrame {
s.updateCrossings(); s.updateCrossings();
sampleWaveform.setAltMarkers(s.getStartCrossing(), s.getEndCrossing()); sampleWaveform.setAltMarkers(s.getStartCrossing(), s.getEndCrossing());
postSentenceGap.setValue(s.getPostGap()); postSentenceGap.setValue(s.getPostGap());
gainPercent.setValue((int)(s.getGain() * 100d));
locked.setSelected(s.isLocked()); locked.setSelected(s.isLocked());
attention.setSelected(s.getAttentionFlag()); attention.setSelected(s.getAttentionFlag());
ethereal.setSelected(s.getEthereal()); ethereal.setSelected(s.getEthereal());
@@ -1510,6 +1556,7 @@ public class AudiobookRecorder extends JFrame {
selectedSentence = null; selectedSentence = null;
sampleWaveform.clearData(); sampleWaveform.clearData();
postSentenceGap.setValue(0); postSentenceGap.setValue(0);
gainPercent.setValue(100);
locked.setSelected(false); locked.setSelected(false);
attention.setSelected(false); attention.setSelected(false);
ethereal.setSelected(false); ethereal.setSelected(false);
@@ -1553,6 +1600,7 @@ public class AudiobookRecorder extends JFrame {
s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.audition.sentence.%08d.locked", i)))); s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.audition.sentence.%08d.locked", i))));
s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.audition.sentence.%08d.attention", i)))); s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.audition.sentence.%08d.attention", i))));
s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.audition.sentence.%08d.ethereal", i)))); s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.audition.sentence.%08d.ethereal", i))));
s.setGain(Utils.s2d(prefs.getProperty(String.format("chapter.audition.sentence.%08d.gain", i))));
bookTreeModel.insertNodeInto(s, c, c.getChildCount()); bookTreeModel.insertNodeInto(s, c, c.getChildCount());
} }
@@ -1573,6 +1621,7 @@ public class AudiobookRecorder extends JFrame {
s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.open.sentence.%08d.locked", i)))); s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.open.sentence.%08d.locked", i))));
s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.open.sentence.%08d.attention", i)))); s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.open.sentence.%08d.attention", i))));
s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.open.sentence.%08d.ethereal", i)))); s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.open.sentence.%08d.ethereal", i))));
s.setGain(Utils.s2d(prefs.getProperty(String.format("chapter.open.sentence.%08d.gain", i))));
bookTreeModel.insertNodeInto(s, c, c.getChildCount()); bookTreeModel.insertNodeInto(s, c, c.getChildCount());
} }
@@ -1599,6 +1648,7 @@ public class AudiobookRecorder extends JFrame {
s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.locked", cno, i)))); s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.locked", cno, i))));
s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.attention", cno, i)))); s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.attention", cno, i))));
s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.ethereal", cno, i)))); s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.ethereal", cno, i))));
s.setGain(Utils.s2d(prefs.getProperty(String.format("chapter.%04d.sentence.%08d.gain", cno, i))));
bookTreeModel.insertNodeInto(s, c, c.getChildCount()); bookTreeModel.insertNodeInto(s, c, c.getChildCount());
} }
} }
@@ -1620,6 +1670,7 @@ public class AudiobookRecorder extends JFrame {
s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.close.sentence.%08d.locked", i)))); s.setLocked(Utils.s2b(prefs.getProperty(String.format("chapter.close.sentence.%08d.locked", i))));
s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.close.sentence.%08d.attention", i)))); s.setAttentionFlag(Utils.s2b(prefs.getProperty(String.format("chapter.close.sentence.%08d.attention", i))));
s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.close.sentence.%08d.ethereal", i)))); s.setEthereal(Utils.s2b(prefs.getProperty(String.format("chapter.close.sentence.%08d.ethereal", i))));
s.setGain(Utils.s2d(prefs.getProperty(String.format("chapter.close.sentence.%08d.gain", i))));
bookTreeModel.insertNodeInto(s, c, c.getChildCount()); bookTreeModel.insertNodeInto(s, c, c.getChildCount());
} }
@@ -1688,13 +1739,13 @@ public class AudiobookRecorder extends JFrame {
} }
public int getNoiseFloorDB() { public int getNoiseFloorDB() {
int nf = getNoiseFloor(); int nf = getNoiseFloor();
if (nf == 0) return 0; if (nf == 0) return 0;
double r = nf / 32767d; double r = nf / 32767d;
double l10 = Math.log10(r); double l10 = Math.log10(r);
double db = 20d * l10; double db = 20d * l10;
return (int)db; return (int)db;
} }
public void recordRoomNoise() { public void recordRoomNoise() {

View File

@@ -32,4 +32,5 @@ public class Icons {
static public final ImageIcon zoomOut = new ImageIcon(Icons.class.getResource("icons/zoom-out.png")); static public final ImageIcon zoomOut = new ImageIcon(Icons.class.getResource("icons/zoom-out.png"));
static public final ImageIcon dollar = new ImageIcon(Icons.class.getResource("icons/dollar.png")); static public final ImageIcon dollar = new ImageIcon(Icons.class.getResource("icons/dollar.png"));
static public final ImageIcon attention = new ImageIcon(Icons.class.getResource("icons/attention.png")); static public final ImageIcon attention = new ImageIcon(Icons.class.getResource("icons/attention.png"));
static public final ImageIcon normalize = new ImageIcon(Icons.class.getResource("icons/normalize.png"));
} }

View File

@@ -47,6 +47,8 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
boolean inSample; boolean inSample;
boolean attention = false; boolean attention = false;
double gain = 1.0d;
String havenJobId = ""; String havenJobId = "";
// 0: Not processed // 0: Not processed
@@ -422,6 +424,10 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
} }
public int[] getAudioDataS16LE(AudioInputStream s, AudioFormat format) throws IOException { public int[] getAudioDataS16LE(AudioInputStream s, AudioFormat format) throws IOException {
return getAudioDataS16LE(s, format, true);
}
public int[] getAudioDataS16LE(AudioInputStream s, AudioFormat format, boolean amplify) throws IOException {
long len = s.getFrameLength(); long len = s.getFrameLength();
int frameSize = format.getFrameSize(); int frameSize = format.getFrameSize();
int chans = format.getChannels(); int chans = format.getChannels();
@@ -441,7 +447,12 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
} else { } else {
sample = (frame[1] << 8) | frame[0]; sample = (frame[1] << 8) | frame[0];
} }
samples[(int)fno] = sample; if (amplify) {
double amped = (double)sample * gain;
samples[(int)fno] = (int)amped;
} else {
samples[(int)fno] = sample;
}
} }
return samples; return samples;
@@ -475,6 +486,28 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
return null; return null;
} }
public int[] getUnprocessedAudioData() {
File f = getFile();
try {
AudioInputStream s = AudioSystem.getAudioInputStream(f);
AudioFormat format = getAudioFormat();
int[] samples = null;
switch (format.getSampleSizeInBits()) {
case 16:
samples = getAudioDataS16LE(s, format, false);
break;
}
s.close();
return samples;
} catch (Exception e) {
}
return null;
}
public int getStartCrossing() { public int getStartCrossing() {
return crossStartOffset; return crossStartOffset;
} }
@@ -606,67 +639,108 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
return null; return null;
} }
byte[] postProcessData(byte[] data) { byte[] adjustGain(byte[] data) {
if (effectEthereal) { AudioFormat format = getAudioFormat();
AudioFormat format = getAudioFormat(); int frameSize = format.getFrameSize();
int frameSize = format.getFrameSize(); int channels = format.getChannels();
int channels = format.getChannels(); int bytesPerChannel = frameSize / channels;
int bytesPerChannel = frameSize / channels;
int frames = data.length / frameSize; int frames = data.length / frameSize;
int byteNo = 0; int byteNo = 0;
double fpms = (double)format.getFrameRate() / 1000d; byte[] out = new byte[data.length];
double doubleOffset = fpms * (double) AudiobookRecorder.window.book.getInteger("effects.ethereal.offset");
int offset = (int)doubleOffset;
double attenuation = 1d - ((double)AudiobookRecorder.window.book.getInteger("effects.ethereal.attenuation") / 100d);
int copies = AudiobookRecorder.window.book.getInteger("effects.ethereal.iterations"); for (int i = 0; i < frames; i++) {
if (channels == 1) {
int l = data[i * frameSize] >= 0 ? data[i * frameSize] : 256 + data[i * frameSize];
int h = data[(i * frameSize) + 1] >= 0 ? data[(i * frameSize) + 1] : 256 + data[(i * frameSize) + 1];
byte[] out = new byte[data.length]; int sample = (h << 8) | l;
if ((sample & 0x8000) == 0x8000) sample |= 0xFFFF0000;
for (int i = 0; i < frames; i++) { double sampleDouble = (double)sample;
if (channels == 1) { sampleDouble *= gain;
int l = data[i * frameSize] >= 0 ? data[i * frameSize] : 256 + data[i * frameSize]; sample = (int)sampleDouble;
int h = data[(i * frameSize) + 1] >= 0 ? data[(i * frameSize) + 1] : 256 + data[(i * frameSize) + 1];
int sample = (h << 8) | l;
if ((sample & 0x8000) == 0x8000) sample |= 0xFFFF0000;
double sampleDouble = (double)sample; if (sample > 32767) sample = 32767;
if (sample < -32768) sample = -32768;
out[i * frameSize] = (byte)(sample & 0xFF);
out[(i * frameSize) + 1] = (byte)((sample & 0xFF00) >> 8);
int used = 0; } else {
for (int j = 0; j < copies; j++) { return data;
if (i + (j * offset) < frames) {
used++;
int lx = data[(i + (j * offset)) * frameSize] >= 0 ? data[(i + (j * offset)) * frameSize] : 256 + data[(i + (j * offset)) * frameSize];
int hx = data[((i + (j * offset)) * frameSize) + 1] >= 0 ? data[((i + (j * offset)) * frameSize) + 1] : 256 + data[((i + (j * offset)) * frameSize) + 1];
int futureSample = (hx << 8) | lx;
if ((futureSample & 0x8000) == 0x8000) futureSample |= 0xFFFF0000;
double futureDouble = (double)futureSample;
for (int k = 0; k < copies; k++) {
futureDouble *= attenuation;
}
sampleDouble = mix(sampleDouble, futureDouble);
}
}
sample = (int)sampleDouble;
if (sample > 32767) sample = 32767;
if (sample < -32768) sample = -32768;
out[i * frameSize] = (byte)(sample & 0xFF);
out[(i * frameSize) + 1] = (byte)((sample & 0xFF00) >> 8);
} else {
return data;
}
} }
return out;
} else {
return data;
} }
return out;
}
byte[] postProcessData(byte[] data) {
data = adjustGain(data);
if (effectEthereal) {
data = processEtherealEffect(data);
}
return data;
}
byte[] processEtherealEffect(byte[] data) {
AudioFormat format = getAudioFormat();
int frameSize = format.getFrameSize();
int channels = format.getChannels();
int bytesPerChannel = frameSize / channels;
int frames = data.length / frameSize;
int byteNo = 0;
double fpms = (double)format.getFrameRate() / 1000d;
double doubleOffset = fpms * (double) AudiobookRecorder.window.book.getInteger("effects.ethereal.offset");
int offset = (int)doubleOffset;
double attenuation = 1d - ((double)AudiobookRecorder.window.book.getInteger("effects.ethereal.attenuation") / 100d);
int copies = AudiobookRecorder.window.book.getInteger("effects.ethereal.iterations");
byte[] out = new byte[data.length];
for (int i = 0; i < frames; i++) {
if (channels == 1) {
int l = data[i * frameSize] >= 0 ? data[i * frameSize] : 256 + data[i * frameSize];
int h = data[(i * frameSize) + 1] >= 0 ? data[(i * frameSize) + 1] : 256 + data[(i * frameSize) + 1];
int sample = (h << 8) | l;
if ((sample & 0x8000) == 0x8000) sample |= 0xFFFF0000;
double sampleDouble = (double)sample;
int used = 0;
for (int j = 0; j < copies; j++) {
if (i + (j * offset) < frames) {
used++;
int lx = data[(i + (j * offset)) * frameSize] >= 0 ? data[(i + (j * offset)) * frameSize] : 256 + data[(i + (j * offset)) * frameSize];
int hx = data[((i + (j * offset)) * frameSize) + 1] >= 0 ? data[((i + (j * offset)) * frameSize) + 1] : 256 + data[((i + (j * offset)) * frameSize) + 1];
int futureSample = (hx << 8) | lx;
if ((futureSample & 0x8000) == 0x8000) futureSample |= 0xFFFF0000;
double futureDouble = (double)futureSample;
for (int k = 0; k < copies; k++) {
futureDouble *= attenuation;
}
sampleDouble = mix(sampleDouble, futureDouble);
}
}
sample = (int)sampleDouble;
if (sample > 32767) sample = 32767;
if (sample < -32768) sample = -32768;
out[i * frameSize] = (byte)(sample & 0xFF);
out[(i * frameSize) + 1] = (byte)((sample & 0xFF00) >> 8);
} else {
return data;
}
}
return out;
} }
public void recognise() { public void recognise() {
@@ -691,7 +765,7 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
public void clearCache() { public void clearCache() {
storedAudioData = null; storedAudioData = null;
System.gc(); // System.gc();
} }
public boolean lockedInCache() { public boolean lockedInCache() {
@@ -926,4 +1000,44 @@ public class Sentence extends DefaultMutableTreeNode implements Cacheable {
return z; return z;
} }
public int getPeakValue() {
int[] samples = getUnprocessedAudioData();
if (samples == null) {
return 0;
}
int ms = 0;
for (int i = 0; i < samples.length; i++) {
if (Math.abs(samples[i]) > ms) {
ms = Math.abs(samples[i]);
}
}
return ms;
}
public int getHeadroom() {
int nf = getPeakValue();
if (nf == 0) return 0;
double r = nf / 32767d;
double l10 = Math.log10(r);
double db = 20d * l10;
return (int)db;
}
public void setGain(double g) {
if (g <= 0.0001d) g = 1.0d;
if (g == gain) return;
gain = g;
clearCache();
}
public double getGain() {
return gain;
}
public void normalize() {
int max = getPeakValue();
setGain(23192d / max);
}
} }

View File

@@ -47,6 +47,14 @@ public class Utils {
return 0.0f; return 0.0f;
} }
public static double s2d(String s) {
try {
return Double.parseDouble(s);
} catch (Exception e) {
}
return 0.0d;
}
public static void browse(String url) { public static void browse(String url) {
if (Desktop.isDesktopSupported()) { if (Desktop.isDesktopSupported()) {
try { try {

View File

@@ -105,12 +105,26 @@ public class Waveform extends JPanel implements MouseListener, MouseMotionListen
if (hcnt > 0) have /= hcnt; if (hcnt > 0) have /= hcnt;
if (lcnt > 0) lave /= lcnt; if (lcnt > 0) lave /= lcnt;
boolean clip = false;
if (lmax > 32000) { // -3dB == 23198?
clip = true;
}
if (hmax > 32000) { // -3dB
clip = true;
}
hmax /= scale; hmax /= scale;
lmax /= scale; lmax /= scale;
have /= scale; have /= scale;
lave /= scale; lave /= scale;
g.setColor(new Color(0, 20, 200)); if (clip) {
g.setColor(new Color(200, 20, 0));
} else {
g.setColor(new Color(0, 20, 200));
}
g.drawLine(n, h/2 + lmax, n, h/2 - hmax); g.drawLine(n, h/2 + lmax, n, h/2 - hmax);
g.setColor(new Color(0, 100, 255)); g.setColor(new Color(0, 100, 255));
g.drawLine(n, h/2 + (int)lave, n, h/2 - (int)have); g.drawLine(n, h/2 + (int)lave, n, h/2 - (int)have);