//////////////////////////////////////////////////////////////////////////////// // Copyright 2013 Michael Schmalle - Teoti Graphix, LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License // // Author: Michael Schmalle, Principal Architect // mschmalle at teotigraphix dot com //////////////////////////////////////////////////////////////////////////////// package com.teotigraphix.caustk.pattern; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; import com.teotigraphix.caustk.library.LibraryPhrase; import com.teotigraphix.caustk.sequencer.SystemSequencer; import com.teotigraphix.caustk.tone.components.PatternSequencerComponent; import com.teotigraphix.caustk.tone.components.PatternSequencerComponent.Resolution; // XXX You have to decide if you are going to proxy the patternsequencer // api through the Tone or call it straight from this class // what makes more sense with UML and no access violations? public class Phrase { public enum Scale { SIXTEENTH, SIXTEENTH_TRIPLET, THIRTYSECOND, THIRTYSECOND_TRIPLET } //-------------------------------------------------------------------------- // Public Property API //-------------------------------------------------------------------------- //---------------------------------- // libraryPhrase //---------------------------------- private LibraryPhrase libraryPhrase; /** * The {@link LibraryPhrase} assigned when the {@link Phrase} was created. */ public LibraryPhrase getLibraryPhrase() { return libraryPhrase; } //---------------------------------- // scale //---------------------------------- private Scale scale = Scale.SIXTEENTH; public Scale getScale() { return scale; } /** * Sets the new scale for the phrase. * <p> * The scale is used to calculate the {@link Resolution} of input. This * property mainly relates the the view of the phrase, where the underlying * pattern sequencer can have a higher resolution but the view is showing * the scale. * <p> * So you can have a phrase scale of 16, but the resolution could be 64th, * but the view will only show the scale notes which would be 16th. * * @param value * @see OnPhraseScaleChange */ public void setScale(Scale value) { if (scale == value) return; Scale oldScale = scale; scale = value; getPart().getPattern().dispatch(new OnPhraseScaleChange(scale, oldScale)); } //---------------------------------- // part //---------------------------------- private Part part; public Part getPart() { return part; } //---------------------------------- // position //---------------------------------- private int position = 1; /** * Returns the current position in the phrase based on the rules of the * view. * <p> * Depending on the resolution and scale, the position can mean different * things. * <p> * 16th note with a length of 4, has 4 measures and thus 4 positions(1-4). * When the position is 2, the view triggers are 16-31(0 index). */ public int getPosition() { return position; } /** * Sets the current position of the phrase. * * @param value The new phrase position. * @see OnPhrasePositionChange */ void setPosition(int value) { if (position == value) return; // if p = 1 and len = 1 if (value < 0 || value > getLength()) return; int oldPosition = position; position = value; getPart().getPattern().dispatch(new OnPhrasePositionChange(position, oldPosition)); } //---------------------------------- // length //---------------------------------- public int getLength() { return getPatternSequencer().getLength(); } /** * Sets the phrase's new length. * <p> * The {@link Pattern#setLength(int)} will set this and when a * {@link Phrase} is committed, the length of the owning pattern is used. * <p> * This should never be set from other than the {@link Pattern}. * * @param value * @see OnPhraseLengthChange */ public void setLength(int value) { int oldValue = getLength(); if (oldValue == value && map.size() != 0) return; if (position > value) setPosition(value); // we 'could' do an event back the the sequencer here but it will // just complicate the friggin code right now so I have decided to // create a Facade on the SynthTone and call it directly. // Doing this would in the future, if I ever wanted to abstract and // create events for this easy, we just take all facade methods and // create events for them // --> ((SynthTone) getPart().getTone()).setLength(value); getPatternSequencer().setLength(value); updateTriggers(value, oldValue); getPart().getPattern().dispatch(new OnPhraseLengthChange(value, oldValue)); } /** * Update the length of the tirggers. * <p> * Only add to the map, triggers are never removed in this version. * * @param oldLength * @param length */ private void updateTriggers(int length, int oldLength) { if (length <= oldLength && map.size() != 0) return; if (oldLength == -1 || map.size() == 0) { int len = Resolution.toSteps(getResolution()) * length; // initialize all triggers to defaults using the current note scale for (int i = 0; i < len; i++) { float beat = Resolution.toBeat(i, getResolution()); //System.out.println("b = " + beat); Trigger trigger = new Trigger(beat, 60, 0.25f, 1f, 0); map.put(beat, trigger); } } else if (length > oldLength) { int startLen = Resolution.toSteps(getResolution()) * oldLength; int len = Resolution.toSteps(getResolution()) * length; // initialize all triggers to defaults using the current note scale for (int i = startLen; i < len; i++) { float beat = Resolution.toBeat(i, getResolution()); //System.out.println("b = " + beat); Trigger trigger = new Trigger(beat, 60, 0.25f, 1f, 0); map.put(beat, trigger); } } } //---------------------------------- // resolution //---------------------------------- public Resolution getResolution() { switch (getScale()) { case SIXTEENTH: return Resolution.SIXTEENTH; case THIRTYSECOND: return Resolution.THIRTYSECOND; default: return Resolution.SIXTYFOURTH; } } public void setResolution(Resolution value) { //getPatternSequencer().setResolution(value); } //---------------------------------- // stepCount //---------------------------------- /** * Returns the full number of steps in all measures. * <p> * IE 4 measures of 32nd resolution has 128 steps. */ public int getStepCount() { int numStepsInMeasure = Resolution.toSteps(getResolution()); return numStepsInMeasure * getLength(); } /** * Returns all the steps in the phrase, no view calculations(position). * <p> * A copy of the collection is returned. */ public List<Trigger> getSteps() { return new ArrayList<Trigger>(map.values()); } public List<Trigger> getViewSteps() { // find the start (position - 1) * resolution int fromStep = (position - 1) * indciesInView; // find the end fromIndex + scale int toStep = endStepInView(fromStep); return new ArrayList<Phrase.Trigger>(map.values()).subList(fromStep, toStep); } /** * Returns the non view step, does not calculate its location within the * view. * * @param step The absolute step position within the phrase. */ public Trigger getStep(int step) { return map.get(Resolution.toBeat(step, getResolution())); } public void setNoteData(String data) { part.getTone().getComponent(PatternSequencerComponent.class).initializeData(data); } //-------------------------------------------------------------------------- // Constructor //-------------------------------------------------------------------------- public Phrase(Part part, LibraryPhrase phraseItem) { this.part = part; this.libraryPhrase = phraseItem; part.setPhrase(this); } //-------------------------------------------------------------------------- // Public Method API //-------------------------------------------------------------------------- public int toAbsoluteStep(int viewStep) { return viewStep + (16 * getPosition()) - 16; // 8 phrase.getResolutin() } /** * Triggers a note at the specified step and will not calculate the view * step. * <p> * This method assumes the step passed is NOT relative or the view step. * * @param step * @param pitch * @param gate * @param velocity * @param flags */ public void triggerOn(int step, int pitch, float gate, float velocity, int flags) { float beat = Resolution.toBeat(step, getResolution()); getPatternSequencer().triggerOn(getResolution(), step, pitch, gate, velocity, flags); Trigger trigger = getTrigger(step); if (trigger == null) { trigger = new Trigger(beat, pitch, gate, velocity, flags); map.put(beat, trigger); } else { trigger.update(beat, pitch, gate, velocity, flags); } trigger.selected = true; } protected void fireChange(TriggerChangeKind kind, Trigger trigger) { getPart().getPattern().dispatch(new OnPhraseTriggerChange(kind, trigger)); } Map<Float, Trigger> map = new TreeMap<Float, Trigger>(); private Trigger getTrigger(float beat) { return map.get(beat); } private Trigger getTrigger(int step) { final float beat = Resolution.toBeat(step, getResolution()); return getTrigger(beat); } public static class OnPhraseTriggerChange { private TriggerChangeKind kind; private Trigger trigger; public final TriggerChangeKind getKind() { return kind; } public final Trigger getTrigger() { return trigger; } public OnPhraseTriggerChange(TriggerChangeKind kind, Trigger trigger) { this.trigger = trigger; } } public enum TriggerChangeKind { RESET, PITCH, GATE, VELOCITY, FLAGS, SELECTED } public class Trigger { private float beat; private int pitch; private float gate; private float velocity; private int flags = 0; public final float getBeat() { return beat; } public final int getPitch() { return pitch; } public final float getGate() { return gate; } public final float getVelocity() { return velocity; } public final int getFlags() { return flags; } private boolean selected = false; public Trigger(float beat, int pitch, float gate, float velocity, int flags) { this.beat = beat; this.pitch = pitch; this.gate = gate; this.velocity = velocity; this.flags = flags; } public void update(float beat, int pitch, float gate, float velocity, int flags) { this.beat = beat; this.pitch = pitch; this.gate = gate; this.velocity = velocity; this.flags = flags; } /** * Returns the step value relative to the containing * {@link Phrase#getResolution()}. */ public int getStep() { return Resolution.toStep(beat, getResolution()); } public int getStep(Resolution resolution) { return Resolution.toStep(beat, resolution); } public boolean isSelected() { return selected; } @Override public String toString() { return "[" + getStep() + "] " + beat + ":" + pitch + "-" + selected; } } public void triggerOn(int step) { Trigger trigger = getStep(step); triggerOn(step, trigger.getPitch(), trigger.getGate(), trigger.getVelocity(), trigger.getFlags()); } public void triggerUpdate(int step, int pitch, float gate, float velocity, int flags) { triggerOff(step); triggerOn(step, pitch, gate, velocity, flags); } public void triggerUpdatePitch(int step, int pitch) { Trigger trigger = getStep(step); triggerUpdate(step, pitch, trigger.getGate(), trigger.getVelocity(), trigger.getFlags()); } public void triggerUpdateGate(int step, float gate) { Trigger trigger = getStep(step); triggerUpdate(step, trigger.getPitch(), gate, trigger.getVelocity(), trigger.getFlags()); } public void triggerUpdateVelocity(int step, float velocity) { Trigger trigger = getStep(step); triggerUpdate(step, trigger.getPitch(), trigger.getGate(), velocity, trigger.getFlags()); } public void triggerUpdateFlags(int step, int flags) { Trigger trigger = getStep(step); triggerUpdate(step, trigger.getPitch(), trigger.getGate(), trigger.getVelocity(), flags); } /** * Triggers a note off at the specified step, this is an absolute step * within the known trigger map's length of measures. * * @param step The absolute step to unselect. */ public void triggerOff(int step) { Trigger trigger = getTrigger(step); getPatternSequencer().triggerOff(getResolution(), step, trigger.getPitch()); trigger.selected = false; } /** * Transposes ALL triggers in the phrase by the delta. * <p> * Note; This is a 'dumb' method in that it does NOT track the last * transposition. The calling client must keep track of the current and last * transpose values to correctly calculate the delta change. * * @see OnPhraseTransposeChange */ public void transpose(int delta) { // XXX Its going to matter if the parent is rhythm or synth eventually for (Trigger trigger : getSteps()) { triggerUpdatePitch(trigger.getStep(), trigger.getPitch() + delta); } getPart().getPattern().dispatch(new OnPhraseTransposeChange(delta)); } /** * Returns whether the absolute(non view) current step is selected. * <p> * Clients MUST calculate the absolute step, this does not factor in length * or position. * * @param step The step to test selection. */ public boolean isSelected(int step) { Trigger trigger = getTrigger(step); if (trigger != null) return trigger.isSelected(); return false; } /** * Increments the internal pointer of the measure position. */ public void incrementPosition() { int len = getLength(); int value = position + 1; if (value > len) value = len; setPosition(value); } /** * Decrement the internal pointer of the measure position. */ public void decrementPosition() { int value = position - 1; if (value < 1) value = 1; setPosition(value); } public void configure() { setResolution(getLibraryPhrase().getResolution()); setNoteData(getLibraryPhrase().getNoteData()); } public void commit() { // TODO Auto-generated method stub } /** * @see SystemSequencer#getDispatcher() */ public static class OnPhraseScaleChange { private Scale scale; private Scale oldScale; public Scale getScale() { return scale; } public Scale getOldScale() { return oldScale; } public OnPhraseScaleChange(Scale scale, Scale oldScale) { this.scale = scale; this.oldScale = oldScale; } } /** * @see SystemSequencer#getDispatcher() */ public static class OnPhrasePositionChange { private int position; private int oldPosition; public int getPosition() { return position; } public int getOldPosition() { return oldPosition; } public OnPhrasePositionChange(int position, int oldPosition) { this.position = position; this.oldPosition = oldPosition; } } /** * @see SystemSequencer#getDispatcher() */ public static class OnPhraseLengthChange { private int length; private int oldLength; public int getLength() { return length; } public int getOldPosition() { return oldLength; } public OnPhraseLengthChange(int length, int oldLength) { this.length = length; this.oldLength = oldLength; } } /** * @see SystemSequencer#getDispatcher() */ public static class OnPhraseTransposeChange { private int delta; public int getDelta() { return delta; } public OnPhraseTransposeChange(int delta) { this.delta = delta; } } protected final PatternSequencerComponent getPatternSequencer() { return getPart().getTone().getComponent(PatternSequencerComponent.class); } private int indciesInView = 16; private int endStepInView(int fromStep) { if (scale == Scale.SIXTEENTH) return fromStep + indciesInView; if (scale == Scale.THIRTYSECOND) return fromStep + indciesInView; return -1; } @Override public String toString() { // return "[Phrase(" // + getPart().getIndex() // + "," // + getPart().getTone().getMachine().getSequencer().getActiveStepPhrase() // .getStepMap().toString() + ")]"; return "[Phrase(" + getPart().getIndex() + ")]"; } }