/*
*
* Copyright (c) 2006-2007 Paul John Leonard
*
* http://www.frinika.com
*
* This file is part of Frinika.
*
* Frinika is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* Frinika is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Frinika; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.frinika.sequencer.model;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
import javax.sound.sampled.AudioFileFormat;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import uk.org.toot.audio.core.AudioBuffer;
import uk.org.toot.audio.core.AudioProcess;
import uk.org.toot.audio.server.AudioServer;
import com.frinika.global.FrinikaConfig;
import com.frinika.project.ProjectContainer;
import com.frinika.project.gui.ProjectFrame;
import com.frinika.sequencer.gui.ItemPanel;
import com.frinika.sequencer.gui.menu.AudioAnalysisAction;
import com.frinika.sequencer.gui.partview.PartView;
import com.frinika.audio.io.AudioReader;
import com.frinika.audio.io.BufferedRandomAccessFile;
import com.frinika.sequencer.model.audio.AudioStreamVoice;
import com.frinika.sequencer.model.audio.EnvelopedAudioReader;
import com.frinika.audio.io.AudioReaderFactory;
import com.frinika.audio.io.VanillaRandomAccessFile;
public class AudioPart extends Part implements AudioReaderFactory {
/**
*
*/
// static char charP[] = { '|', '/', '-', '\\' };
private static final long serialVersionUID = 1L;
private String audioFileName;
private String audioDir;
/**
* Start time relative to sequencer zero time (in microseconds)
*/
double realStartTimeInMicros = 0;
// size of clip frames*channels ?
// private float playBufferSize;
transient AudioStreamVoice outputProcess = null;
transient int nChannel;
transient private Image thumbNailImage = null;
transient private AudioReader thumbNailIn = null; // reader for the thumb
// nail.
transient private int buffSize; // audio file cache size
transient private ThumbNailRunnable thumbNailRunnable;
static AudioFileFormat.Type targetType = AudioFileFormat.Type.WAVE;
transient EnvelopedAudioReader audioPlayerIn;
Envelope envelope;
public AudioPart(AudioLane lane) {
super(lane);
init();
}
// public AudioPart(AudioPart clone) {
// super(clone.getLane());
// audioDir = clone.audioDir;
// audioFileName = clone.audioFileName;
// realStartTime = clone.realStartTime;
// envelope = clone.envelope;
// File clipFile = new File(audioDir, audioFileName);
// }
public AudioPart() {
init();
}
private void init() {
realStartTimeInMicros = 0;
// lengthInMicros = 0;
outputProcess = null;
nChannel = 1;
buffSize = 100000; // audio file cache size TODO what is the best
// number ?
}
/**
* Creates a new AudioPart. To use the AudioPart call onLoad() (normally
* done be the AudioLane).
*
* @param lane
* add part to this lane. Can be null for a detached part.
* @param clipFile
* .wav file of audio
* @param startTimeInMicros
* postition first sample in micros
*
*/
public AudioPart(Lane lane, File clipFile, double startTimeInMicros) {
super(lane);
init();
audioDir = clipFile.getParent();
audioFileName = clipFile.getName();
realStartTimeInMicros = startTimeInMicros;
if (!clipFile.exists()) {
try {
System.err.println(" Missing audio file "
+ clipFile.getCanonicalPath());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
clipFile = null;
}
createFileHandles(clipFile);
}
private void createFileHandles(File clipFile) {
AudioServer audioServer = lane.getProject().getAudioServer();
// System. out.println(" Creating handles");
boolean newEnvelope = envelope == null;
if (newEnvelope) {
envelope = new Envelope();
}
double lengthInMicros = 0;
if (!(clipFile == null)) {
try {
RandomAccessFile raf = new RandomAccessFile(clipFile, "r");
BufferedRandomAccessFile braf = new BufferedRandomAccessFile(
raf, buffSize, lane.getProject().getAudioFileManager());
audioPlayerIn = new EnvelopedAudioReader(braf,FrinikaConfig.sampleRate);
// in = new AudioReader(new VanillaRandomAccessFile(raf));
RandomAccessFile rafG = new RandomAccessFile(clipFile, "r");
thumbNailIn = new AudioReader(new VanillaRandomAccessFile(rafG),FrinikaConfig.sampleRate);
if (audioPlayerIn.getFormat().getSampleRate() != FrinikaConfig.sampleRate) {
try {
throw new Exception(" unsupport format " + audioPlayerIn.getFormat() );
} catch(Exception e) {
e.printStackTrace();
}
}
lengthInMicros = audioPlayerIn.getLengthInFrames()
/ audioPlayerIn.getFormat().getSampleRate() * 1000000.0;
System.out.println("audioPart:"+ clipFile +" " + lengthInMicros/1000000.0 + " secs");
outputProcess = new AudioStreamVoice(audioServer, lane
.getProject().getSequencer(), audioPlayerIn,
(long) realStartTimeInMicros);
nChannel = audioPlayerIn.getFormat().getChannels();
// playBufferSize *= nChannel;
// AudioContext.getDefaultAudioContext().getVoiceServer()
// .addTransmitter(oscStr);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// maxtime is not persisted so we must deduce from the file
envelope.setMaxTime(lengthInMicros);
if (newEnvelope) {
envelope.setTOn(0.0);
envelope.setTOff(lengthInMicros);
}
refreshEnvelope();
}
public void refreshEnvelope() {
if (envelope == null)
return;
envelope.validate();
if (audioPlayerIn == null)
return;
audioPlayerIn.setEvelope(envelope);
thumbNailIn.setBoundsInMicros(envelope.tOn, envelope.tOff);
}
public File getAudioFile() {
return new File(audioDir, audioFileName);
}
@Override
protected void moveItemsBy(long deltaTick) {
assert (false);
// TODO Auto-generated method stub
}
@Override
public void addToModel() {
super.addToModel();
System.out.println(" Adding " + this + " to model ");
try {
onLoad();
} catch (FileNotFoundException e) {
e.printStackTrace();
removeFromModel();
}
}
/*
*
* Create a new AudioPart but do not attach to the project.
*
* (non-Javadoc)
*
* @see com.frinika.sequencer.model.Part#clone()
*/
@Override
public Object clone() throws CloneNotSupportedException {
AudioPart clone = new AudioPart();
clone.audioDir = audioDir;
clone.audioFileName = audioFileName;
clone.realStartTimeInMicros = realStartTimeInMicros;
clone.envelope = (Envelope) envelope.clone();
// clone.lane=lane;
File clipFile = new File(audioDir, audioFileName);
return clone;
}
public void restoreFromClone(EditHistoryRecordable object) {
AudioPart clone = (AudioPart) object;
audioDir = clone.audioDir;
audioFileName = clone.audioFileName;
realStartTimeInMicros = clone.realStartTimeInMicros;
envelope = clone.envelope;
// lane=clone.lane;
File clipFile = new File(audioDir, audioFileName);
}
@Override
public void copyBy(double tick, Lane dst) {
AudioPart clone;
try {
clone = new AudioPart((AudioLane) dst, new File(audioDir,
audioFileName), (long) realStartTimeInMicros);
clone.envelope = (Envelope) envelope.clone();
clone.onLoad();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/*
*
*
* (non-Javadoc)
*
* @see com.frinika.sequencer.model.Selectable#deepCopy(com.frinika.sequencer.model.Selectable)
*/
public Selectable deepCopy(Selectable parent) {
AudioPart clone = new AudioPart((AudioLane) parent);
// clone.startTick=startTick;
// clone.endTick=endTick;
clone.audioDir = audioDir;
clone.audioFileName = audioFileName;
clone.realStartTimeInMicros = realStartTimeInMicros;
clone.envelope = (Envelope) envelope.clone();
if (parent == null) {
clone.lane = lane;
}
// clone.name="Copy of "+name;
clone.color = color;
return clone;
}
public void deepMove(long dTick) {
double tick1 = lane.getProject().tickAtMicros(realStartTimeInMicros);
realStartTimeInMicros = (long) lane.getProject().microsAtTick(tick1 + dTick);
}
/*
* public AudioDeviceHandle getAudioDeviceHandle() { // TODO Auto-generated
* method stub return ((AudioLane)lane).getAudioInDevice(); }
*/
/**
*
* @return length of the part in ticks
*/
// public long getDurationInTicks() {
// return microsToTick(envelope.tOff - envelope.tOn);
// // return microsToTick(lengthInMicros);
// }
// private long microsToTick(double micros) {
// FrinikaSequence seq = lane.getProject().getSequence();
// FrinikaSequencer sequ = lane.getProject().getSequencer();
// return (long) (((seq.getResolution() * sequ.getTempoInBPM() * micros) / 60.0) / 1000000.0);
// }
/**
*
* @return start of the part in ticks
*/
public long getStartTick() {
return (long) lane.getProject().getTempoList().getTickAtTime((realStartTimeInMicros + envelope.tOn)/1000000.0);
}
/**
*
* @return end tick of the part
*/
public long getEndTick() {
return (long) lane.getProject().getTempoList().getTickAtTime((realStartTimeInMicros + envelope.tOff)/1000000.0);
}
/**
*
* @return length of the part in secs
*/
public double getDurationInSecs(){
return (envelope.tOff-envelope.tOn)/1000000.0;
}
/**
*
* @return start of the part in secs
*/
public double getStartInSecs() {
return (realStartTimeInMicros + envelope.tOn)/1000000.0;
}
/**
*
* @return end of the part in secs
*/
public double getEndInSecs() {
return (realStartTimeInMicros + envelope.tOff)/1000000.0;
}
/*
* Detach output oscillator from the voiceserver
*
* (non-Javadoc)
*
* @see com.frinika.sequencer.model.Part#commitEventsRemove()
*/
public void commitEventsRemove() {
}
/*
* Attach output oscillator from the voiceserver
*
* (non-Javadoc)
*
* @see com.frinika.sequencer.model.Part#commitEventsAdd()
*/
public void commitEventsAdd() {
refreshEnvelope();
}
public void onLoad() throws FileNotFoundException {
File clipFile = new File(audioDir, audioFileName);
if (!clipFile.exists()) {
try {
System.err.println(" Missing audio file "
+ clipFile.getCanonicalPath());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
clipFile = null;
}
createFileHandles(clipFile);
}
// /**
// *
// * A general non real time purpose reader for the audio data.
// *
// * @return
// * @throws IOException
// * @deprecated use createAudioReader
// */
// public AudioClipReader createClipReader() throws IOException {
// File clipFile = new File(audioDir, audioFileName);
// AudioServer server = FrinikaAudioSystem.getAudioServer();
// double fs = server.getSampleRate();
// return new AudioClipReader(clipFile,
// (long) ((realStartTimeInMicros * fs) / 1000000.0));
// }
/**
* A non realtime Reader for a raw view at the data.
*
* @return
* @throws IOException
*/
public AudioReader createAudioReader() throws IOException {
File clipFile = new File(audioDir, audioFileName);
VanillaRandomAccessFile rafG = new VanillaRandomAccessFile(
new RandomAccessFile(clipFile, "r"));
AudioReader reader=new AudioReader(rafG,FrinikaConfig.sampleRate);
reader.setBoundsInMicros(envelope.tOn, envelope.tOff);
return reader;
}
// @Override
// public void attach() {
//
// }
//
// @Override
// public void detach() {
//
// }
private void reconstructThumbNail(Rectangle rect, PartView panel) {
if (thumbNailRunnable == null) {
thumbNailRunnable = new ThumbNailRunnable();
Thread t = new Thread(thumbNailRunnable);
t.setPriority(Thread.MIN_PRIORITY);
thumbNailRunnable.setThread(t);
t.start();
}
thumbNailRunnable.reconstruct(rect, panel);
}
transient Rectangle lastRect = null;
public void drawThumbNail(Graphics2D g, Rectangle rect, PartView panel) {
// System. out.println(" Draw thumb nail 1");
if (outputProcess == null || thumbNailIn == null)
return;
if (thumbNailImage == null || lastRect == null
|| rect.width != lastRect.width
|| rect.height != lastRect.height) {
// System. out.println(" Draw thumb nail 2");
lastRect = (Rectangle) rect.clone();
reconstructThumbNail(lastRect, panel);
}
g.setXORMode(Color.WHITE);
g.drawImage(thumbNailImage, rect.x, rect.y, null);
g.setPaintMode();
}
public void moveContentsBy(double dTick, Lane dstLane) {
double tick1 = lane.getProject().tickAtMicros(realStartTimeInMicros);
realStartTimeInMicros = (long) lane.getProject().microsAtTick(tick1 + dTick);
// commitEventsRemove();
if (dstLane != lane) {
lane.getParts().remove(this);
dstLane.getParts().add(this);
lane = dstLane;
}
outputProcess.setRealStartTime((long) realStartTimeInMicros);
// commitEventsAdd();
}
public String toString() {
String ret = hashCode() + ":" + audioDir + "/" + audioFileName + "|"
+ realStartTimeInMicros;
return ret;
}
/**
* @return output process to play audio
*/
public AudioProcess getAudioProcess() {
return outputProcess;
}
@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
init();
in.defaultReadObject();
}
class ThumbNailRunnable implements Runnable {
Rectangle rect;
PartView panel;
Thread runThread;
Graphics2D gg;
public void reconstruct(Rectangle rect, PartView panel) {
this.rect = rect;
this.panel = panel;
if (runThread.isInterrupted()) {
// System. out.println(" Thunmb nail thread already
// interupted");
return;
}
// System. out.println(" Reconstruct ");
runThread.interrupt();
}
public void setThread(Thread t) {
runThread = t;
}
synchronized boolean buildThumbNail() {
// System. out.println(" buildThumbNail ");
// reconstruct code
thumbNailImage = new BufferedImage(rect.width, rect.height,
BufferedImage.TYPE_BYTE_BINARY);
gg = (Graphics2D) thumbNailImage.getGraphics();
int y = rect.height / 2;
gg.drawString("...", 0, 5);
panel.setDirty();
// panel.repaint();
double x = getStartInSecs();
double w = getDurationInSecs();
try {
thumbNailIn.seekFrame(0, false);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
int nChannel = thumbNailIn.getFormat().getChannels();
long nFrame = thumbNailIn.getLengthInFrames();
ProjectContainer project = lane.getProject();
// double ticksPerSecond = project.getSequence().getResolution()
// * (project.getSequencer().getTempoInBPM() / 60.0);
// double framePerTick = FrinikaConfig.sampleRate / ticksPerSecond;
double sampleToScreen = panel.userToScreen / FrinikaConfig.sampleRate;
// int n = nChannel * 2;
int chunkSize = 1024;
// long nBytes = n * nFrame;
// byte byteBuffer[] = new byte[chunkSize]; // 1k chunks ?
AudioBuffer buff = new AudioBuffer("thumbnail", nChannel,
chunkSize, 44100.0f);
int nRead = 0;
double valMax = 0;
double valMin = 0;
// double scale = rect.height / 32768.0 / 2.0;
double scale = rect.height / 2.0;
gg.setColor(Color.white);
int midY = rect.height / 2;
int pix = 0;
int ii = 0;
int cc = 0;
try {
thumbNailIn.seekTimeInMicros(envelope.tOn, false);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
while (nRead < nFrame) {
if (runThread.isInterrupted()) {
// System. out.println("Interupted .....");
return false;
}
int nn = chunkSize;
if (nRead + chunkSize > nFrame)
nn = (int) (nFrame - nRead);
buff.makeSilence();
thumbNailIn.processAudio(buff);
nRead += nn;
if (nChannel == 2) {
float left[] = buff.getChannel(0);
float right[] = buff.getChannel(1);
for (int i = 0; i < nn; i++, ii++) {
float sampleL = left[i];
float sampleR = right[i];
valMin = Math.min(Math.min(valMin, sampleL), sampleR);
valMax = Math.max(Math.max(valMax, sampleL), sampleR);
int pixNow = (int) (ii * sampleToScreen);
if (pixNow > pix) {
gg.drawLine(pix, (int) (midY + valMin * scale),
pix, (int) (midY + valMax * scale));
pix = pixNow;
valMax = valMin = 0;
}
}
} else {
float left[] = buff.getChannel(0);
for (int i = 0; i < nn; i++, ii++) {
float sampleL = left[i];
valMin = Math.min(valMin, sampleL);
valMax = Math.max(valMax, sampleL);
int pixNow = (int) (ii * sampleToScreen);
if (pixNow > pix) {
gg.drawLine(pix, (int) (midY + valMin * scale),
pix, (int) (midY + valMax * scale));
pix = pixNow;
valMax = valMin = 0;
}
}
}
}
Rectangle2D brect = gg.getFontMetrics().getStringBounds(
audioFileName, gg);
gg.setColor(Color.BLACK);
gg.fillRect(1, 0, (int) brect.getWidth(), (int) brect.getHeight());
gg.setColor(Color.WHITE);
gg.drawString(audioFileName, 1, (int) brect.getHeight());
// System .out.println(" Thumbnail reconstructed ");
panel.setDirty();
panel.repaint();
// System. out.println(" BUILD DONE");
return true;
}
synchronized public void run() {
assert (runThread == Thread.currentThread());
while (true) {
try {
wait();
} catch (InterruptedException e1) {
while (!buildThumbNail())
Thread.interrupted(); // clear interrupt flag
}
}
}
}
public void setStartTick(long tick) {
envelope.setTOn(lane.getProject().microsAtTick(tick) - realStartTimeInMicros);
}
/**
*
* @param tick
* new end tick for display purpose only
*/
public void setEndTick(long tick) {
envelope.setTOff(lane.getProject().microsAtTick(tick) - realStartTimeInMicros);
}
static double minT = 1;
static int danc = 1;
static Rectangle anc = new Rectangle(0, 0, 3 * danc, 3 * danc);
final public class Envelope implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
double tOn;
double tRise;
double gain;
double tOff;
double tFall;
transient double maxTime;
// public Envelope(double tOn,double tOff) {
// System. out.println(" New envelope " + tOn + " " + tOff);
// this.tOn=tOn;
// this.tOff=tOff;
// gain=1.0;
// tRise=0.0;
// tFall=0.0;
// // TODO validate these values
// }
public void validate() {
// TODO Auto-generated method stub
// make sure everything is OK.
assert (tOn <= tOff);
}
public Envelope() {
gain = 1.0;
}
public double getGain() {
return gain;
}
public void setGain(double gain) {
System.out.println(" SET GAIN " + gain);
if (gain > 1.0) {
this.gain = 1.0;
} else if (gain < 0.0) {
this.gain = 0.0;
} else {
this.gain = gain;
}
}
public double getTOff() {
return tOff;
}
public void setTOff(double off1) {
tOff = Math.min(off1, maxTime);
}
public double getTOn() {
return tOn;
}
public void setTOn(double on1) {
tOn = Math.max(0, on1);
}
public Object clone() {
Envelope clone = new Envelope();
clone.tOn = tOn;
clone.tOff = tOff;
clone.gain = gain;
clone.tRise = tRise;
clone.tFall = tFall;
clone.maxTime = maxTime;
return clone;
}
@SuppressWarnings("unchecked")
private void readObject(ObjectInputStream in)
throws ClassNotFoundException, IOException {
// System.out.println(" READING ENVELOPE ");
in.defaultReadObject();
}
public double getMaxTime() {
return maxTime;
}
public void setMaxTime(double maxTime) {
this.maxTime = maxTime;
}
public void draw(Graphics2D g, Rectangle rect, PartView view) {
double xt = tOff - tOn;
int x1 = rect.x;
int y1 = rect.y + rect.height;
int x4 = rect.x + rect.width;
int y4 = y1;
int x2 = (int) (x1 + tRise / xt * rect.width);
int y2 = (int) (rect.y + rect.height * (1.0 - gain));
int x3 = (int) (x4 - tFall / xt * rect.width);
int y3 = y2;
Stroke stroke = g.getStroke();
g.setStroke(new BasicStroke(2));
g.setColor(Color.BLUE);
g.drawLine(x1, y1, x2, y2);
g.drawLine(x2, y2, x3, y3);
g.drawLine(x3, y3, x4, y4);
g.setStroke(stroke);
g.setColor(Color.YELLOW);
anc.setLocation(x2 - danc, y2 - danc);
g.fill(anc);
anc.setLocation(x3 - danc, y3 - danc);
g.fill(anc);
}
public int getHoverState(Point p, Rectangle rect) {
int tol = 4;
double xt = tOff - tOn;
int y2 = (int) (rect.y + (1.0 - gain) * rect.height);
if (Math.abs(p.y - y2) > tol)
return -1;
int x1 = rect.x;
int y1 = rect.y + rect.height;
int x4 = rect.x + rect.width;
int y4 = y1;
int x2 = (int) (x1 + tRise / xt * rect.width);
if (Math.abs(p.x - x2 - tol) < tol)
return ItemPanel.OVER_ENVELOPE_LEFT;
int x3 = (int) (x4 - tFall / xt * rect.width);
int y3 = y2;
if (Math.abs(p.x - x3 + tol) < tol)
return ItemPanel.OVER_ENVELOPE_RIGHT;
return ItemPanel.OVER_ENVELOPE_GAIN;
}
public void setTOffRel(double fact) {
if (fact < 0.0 || fact > 1.0)
return;
tFall = (1.0 - fact) * (tOff - tOn);
}
public void setTOnRel(double fact) {
if (fact < 0.0 || fact > 1.0)
return;
tRise = fact * (tOff - tOn);
}
public double getTFall() {
return tFall;
}
public double getTRise() {
return tRise;
}
public void setTFall(double fall) {
tFall = fall;
}
public void setTRise(double rise) {
tRise = rise;
}
}
public void drawEnvelope(Graphics2D g, Rectangle rect, PartView view) {
envelope.draw(g, rect, view);
}
public int getHoverState(Point p, Rectangle rect) {
return envelope.getHoverState(p, rect);
}
public Envelope getEvelope() {
return envelope;
}
/**
* To be extended by subclasses.
*
* @param popup
*/
@Override
protected void initContextMenu(final ProjectFrame frame, JPopupMenu popup) {
JMenuItem item = new JMenuItem(new AudioAnalysisAction(frame));
popup.add(item);
// super.initContextMenu(frame, popup); do NOT call super.init...,
// because we don't want a "Properties..." for audio parts (yet)
}
public void setStartInSecs(double start) {
envelope.setTOn(start*1000000.0 - realStartTimeInMicros);
//
// try {
// throw new Exception(" IMplement me");
// } catch (Exception e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
}
public void setEndInSecs(double end) {
envelope.setTOff(1000000.0*end - realStartTimeInMicros);
}
}