/* * Created on Mar 6, 2007 * * Copyright (c) 2007 Jens Gulden * * 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.midi.groovepattern; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import javax.sound.midi.InvalidMidiDataException; import javax.sound.midi.MidiEvent; import javax.sound.midi.MidiMessage; import javax.sound.midi.MidiSystem; import javax.sound.midi.Sequence; import javax.sound.midi.ShortMessage; import javax.sound.midi.Track; import com.frinika.project.ProjectContainer; import com.frinika.project.gui.ProjectFrame; import com.frinika.sequencer.model.MidiPart; import com.frinika.sequencer.model.MultiEvent; import com.frinika.sequencer.model.NoteEvent; /** * Groove pattern derived from midi data. * * The midi data must at least contain one event every beat, otherwise its size will * not be detected properly. * * @author Jens Gulden */ public class GroovePatternFromSequence implements GroovePattern { protected String name; protected Sequence sequence; protected long length; // rounded as whole beats protected int lengthInBeats; protected int notesCount; public GroovePatternFromSequence() { super(); } public GroovePatternFromSequence(String name, Sequence sequence) { this(); setName(name); setSequence(sequence); } public String getName() { return name; } public void setName(String name) { this.name = name; } public Sequence getSequence() { return sequence; } public void setSequence(Sequence sequence) { this.sequence = sequence; // calculate length in beats and notes count Track track = getTrack(); notesCount = 0; long start = -1; long end = -1; int size = track.size(); if (size > 0) { for (int i = 0; i < size; i++) { MidiEvent ev = track.get(i); MidiMessage msg = ev.getMessage(); if ((msg instanceof ShortMessage) && (((ShortMessage)msg).getCommand() == ShortMessage.NOTE_ON)) { notesCount++; if (start == -1) { start = ev.getTick(); } end = ev.getTick(); } } int resolution = sequence.getResolution(); long b1 = (start + (resolution/4)) / resolution; // +res/2: allow notes to start a bit earlier but still be counted to other bar long b2 = (end - (resolution/4)) / resolution; // - res/2: ... lengthInBeats = (int)(b2 - b1 + 1); length = lengthInBeats * resolution; } else { length = 0; } } public long quantize(long tick, int quantizeResolution, float smudge, int[] velocityByRef) { long t = tick % length; // tick inside pattern, to be (de-)quantized (length is rounded as whole beats) long diff = tick - t; // add again later long q = findNearest(t, velocityByRef); long qDiff = t-q; if (qDiff < 0) qDiff = -qDiff; if (qDiff > quantizeResolution) { // didn't find a tick close enough in pattern: use original tick without groove (de-)quantization (should it be quantizeResolution/2 ?) return tick; } else { if (smudge != 0.0f) { int[] v2 = new int[1]; long q2 = findFurthest(t, quantizeResolution, v2); long qDiff2 = t-q2; if (qDiff2 > qDiff) { // alternative possible value long d = q2 - q; d = Math.round(d * smudge); q += d; int n = v2[0] - velocityByRef[0]; velocityByRef[0] += Math.round(n * smudge); } } return q + diff; } } private long findNearest(long tick, int[] v) { if (notesCount == 0) return tick; Track track = getTrack(); int size = track.size(); int shift = 4 * sequence.getResolution(); // pattern starts at 4 * ppq long t = tick % length; // tick inside pattern, to be (de-)quantized t = tick + shift; int i = 0; long nearest = -1; v[0] = 100; long tt = t; MidiEvent ev; boolean isNote; do { ev = track.get(i++); tt = ev.getTick(); MidiMessage msg = ev.getMessage(); isNote = (msg instanceof ShortMessage) && (((ShortMessage)msg).getCommand() == ShortMessage.NOTE_ON); if ((isNote) && ( ( tt < t ) || ( (tt-t) < (t-nearest) ) ) ) { nearest = tt; v[0] = ((ShortMessage)msg).getData2(); } } while ((i < size) && ((!isNote) || (tt < t))); // stops after first note imediately following after (tt<t) no longer is true return nearest - shift; } private long findFurthest(long tick, int q, int[] v) { if (notesCount == 0) return tick; Track track = getTrack(); int size = track.size(); int shift = 4 * sequence.getResolution(); // pattern starts at 4 * ppq long t = tick % length; // tick inside pattern, to be (de-)quantized t = tick + shift; int i = 0; long furthest = t; v[0] = 100; int q2 = q/2; long tt; MidiEvent ev; while ((i < size) && ( (tt = (ev = track.get(i++)).getTick()) < t+q2)) { MidiMessage msg = ev.getMessage(); if ((msg instanceof ShortMessage) && (((ShortMessage)msg).getCommand() == ShortMessage.NOTE_ON)) { long d = t - tt; if (d < 0) d = -d; if (d <= q2) { if (abs(furthest-t) < d) { // found something in allowed range, and even further away furthest = tt; v[0] = ((ShortMessage)msg).getData2(); } } } } return furthest - shift; } private static long abs(long a) { if (a < 0) { return -a; } else { return a; } } /*public int velocity(long tick) { Track track = getTrack(); int size = track.size(); if (size <= 0) return 100; // 100 is default if not found int shift = 4 * sequence.getResolution(); // pattern starts at 4 * ppq //long length = getTrack().ticks() - shift; //int ticksPerBeat = sequence.getResolution(); //length = ((length / ticksPerBeat) +1) * ticksPerBeat; // length rounded as whole beats long t = tick % length; // tick inside pattern t = tick + shift; MidiEvent ev = track.get(0); long tt = ev.getTick(); int i = 1; long nearest = tt; int vel = 100; // 100 is default if not found while ((i < size) && (tt <= t)) { ev = track.get(i++); tt = ev.getTick(); nearest = tt; } if (tt >= t) { if ((tt - t) < (t - nearest)) { nearest = tt; } } return nearest - shift; }*/ public void importFromMidiPart(String name, MidiPart part) throws IOException { this.sequence = null; this.name = GroovePatternManager.normalizeName(name); try { sequence = new Sequence(Sequence.PPQ, part.getLane().getProject().getSequence().getResolution(), 1); normalize(part, sequence); setSequence(sequence); // to update properties } catch (InvalidMidiDataException imde) { imde.printStackTrace(); throw new IOException("unable to import MIDI part as groove pattern"); } } public void importFromMidiFile(String name, InputStream in) throws IOException { this.sequence = null; this.name = GroovePatternManager.normalizeName(name); try { Sequence seq = MidiSystem.getSequence(in); if (seq == null) { throw new IOException("unable to load groove pattern " + name); } sequence = new Sequence(Sequence.PPQ, seq.getResolution(), 1); normalize(seq, sequence); setSequence(sequence); // to update properties } catch (InvalidMidiDataException imde) { imde.printStackTrace(); throw new IOException("unable to load groove pattern " + name + " - " + imde.getMessage()); } } public void importFromMidiFile(File file) throws IOException { String name = file.getName(); int dot = name.lastIndexOf('.'); if (dot != 1) { name = name.substring(0, dot); } FileInputStream in = new FileInputStream(file); importFromMidiFile(name, in); in.close(); } public void saveAsMidiFile(File file) throws IOException { MidiSystem.write(sequence, 1, file); } public void openAsOwnProject() throws Exception { ProjectContainer newProject = new ProjectContainer(sequence); ProjectFrame newProjectFrame = new ProjectFrame(newProject); } /** * Copies all note-events from the MidiPart to track 0, channel 0, starting at offset 4 beats (offset to cleanly capture negative shifts of the very first beat) * @param part * @param sequence empty 1-track sequence to be filled */ static void normalize(MidiPart part, Sequence sequence) { int resolutionPart = part.getLane().getProject().getSequence().getResolution(); int resolutionSeq = sequence.getResolution(); Track track = sequence.getTracks()[0]; boolean firstNote = true; long shift = 4 * resolutionPart; // offset start: 4 beats for (MultiEvent ev : part.getMultiEvents()) { if (ev instanceof NoteEvent) { NoteEvent n = (NoteEvent)ev; long start = n.getStartTick(); long end = n.getEndTick(); int note = n.getNote(); int vel = n.getVelocity(); if (firstNote) { shift -= ((start + (resolutionPart / 4)) / resolutionPart) * resolutionPart ; // correct offset by leading number of empty beats (so pattern will always start in first beat (or a little earlier for negative groove-shifts, this is why resolution/2 is added)) firstNote = false; } start += shift; end += shift; if (resolutionPart != resolutionSeq) { start = translateResolution(start, resolutionPart, resolutionSeq); end = translateResolution(end, resolutionPart, resolutionSeq); } try { ShortMessage sm = new ShortMessage(); sm.setMessage(ShortMessage.NOTE_ON, 0, note, vel); MidiEvent event = new MidiEvent(sm, start); track.add(event); sm = new ShortMessage(); sm.setMessage(ShortMessage.NOTE_OFF, 0, note, vel); event = new MidiEvent(sm, end); track.add(event); } catch (InvalidMidiDataException imde) { imde.printStackTrace(); } } } } /** * Copies all note-events from seq to track 0, channel 0, starting at offset 4 beats * * (Might fail if multiple tracks are imported and a track other than #0 has an earlier * note than track #0. However, this method is mainly intended for loading internally * saved grooved patterns.) * * @param part * @param sequence empty 1-track sequence to be filled */ static void normalize(Sequence seq, Sequence sequence) { int srcRes = seq.getResolution(); int dstRes = sequence.getResolution(); Track track = sequence.getTracks()[0]; boolean firstNote = true; long shift = 4 * srcRes; // offset start: 4 beats Track[] srcTracks = seq.getTracks(); for (int i = 0; i < srcTracks.length; i++) { Track srcTrack = srcTracks[i]; int size = srcTrack.size(); for (int j = 0; j < size; j++) { MidiEvent ev = srcTrack.get(j); MidiMessage msg = ev.getMessage(); if (msg instanceof ShortMessage) { ShortMessage sh = (ShortMessage)msg; int cmd = sh.getCommand(); if (cmd == ShortMessage.NOTE_ON || cmd == ShortMessage.NOTE_OFF) { int note = sh.getData1(); int vel = sh.getData2(); long start = ev.getTick(); if (firstNote) { shift -= ((start + (srcRes / 4)) / srcRes) * srcRes ; // correct offset by leading number of empty beats (so pattern will always start in first beat (or a little earlier for negative groove-shifts, this is why resolution/2 is added)) firstNote = false; } start += shift; if (srcRes != dstRes) { start = translateResolution(start, srcRes, dstRes); } // insert new event into target sequence try { ShortMessage sm = new ShortMessage(); sm.setMessage(cmd, 0, note, vel); MidiEvent event = new MidiEvent(sm, start); track.add(event); } catch (InvalidMidiDataException imde) { imde.printStackTrace(); } } } } } } static long translateResolution(long tick, int srcRes, int destRes) { return Math.round( ((double)tick / srcRes) * destRes ); } /** * For displaying in GroovePatternManagerDialog's list. */ public String toString() { return getName() + " [" + lengthInBeats +" beats, " + notesCount + " notes]"; } public Track getTrack() { return sequence.getTracks()[0]; } }