changeset 35:10ba9276a315

* Add TextModel and TextLayer types * Make View refresh work better when editing a model (previously edits might not be refreshed if their visible changed area extended beyond the strict frame range that was being modified in the model) * Add phase-adjusted instantaneous frequency display to spectrogram layer (still a work in progress) * Pull maths aliases out into a separate header in dsp/maths so MathUtilities can be included without introducing them
author Chris Cannam
date Mon, 20 Feb 2006 13:33:36 +0000
parents c43f2c4f66f2
children c28ebb4ba4de
files layer/LayerFactory.cpp layer/LayerFactory.h layer/SpectrogramLayer.cpp layer/SpectrogramLayer.h layer/TextLayer.cpp layer/TextLayer.h widgets/Pane.cpp widgets/PropertyBox.cpp
diffstat 8 files changed, 1190 insertions(+), 57 deletions(-) [+]
line wrap: on
line diff
--- a/layer/LayerFactory.cpp	Fri Feb 17 18:11:08 2006 +0000
+++ b/layer/LayerFactory.cpp	Mon Feb 20 13:33:36 2006 +0000
@@ -15,6 +15,7 @@
 #include "TimeInstantLayer.h"
 #include "TimeValueLayer.h"
 #include "NoteLayer.h"
+#include "TextLayer.h"
 #include "Colour3DPlotLayer.h"
 
 #include "model/RangeSummarisableTimeValueModel.h"
@@ -22,6 +23,7 @@
 #include "model/SparseOneDimensionalModel.h"
 #include "model/SparseTimeValueModel.h"
 #include "model/NoteModel.h"
+#include "model/TextModel.h"
 #include "model/DenseThreeDimensionalModel.h"
 
 LayerFactory *
@@ -47,6 +49,7 @@
     case TimeInstants: return Layer::tr("Time Instants");
     case TimeValues:   return Layer::tr("Time Values");
     case Notes:        return Layer::tr("Notes");
+    case Text:         return Layer::tr("Text");
     case Colour3DPlot: return Layer::tr("Colour 3D Plot");
 
     case MelodicRangeSpectrogram:
@@ -85,10 +88,14 @@
 
     if (dynamic_cast<SparseTimeValueModel *>(model)) {
 	types.insert(TimeValues);
+    
+}
+    if (dynamic_cast<NoteModel *>(model)) {
+	types.insert(Notes);
     }
 
-    if (dynamic_cast<NoteModel *>(model)) {
-	types.insert(Notes);
+    if (dynamic_cast<TextModel *>(model)) {
+	types.insert(Text);
     }
 
     // We don't count TimeRuler here as it doesn't actually display
@@ -104,6 +111,7 @@
     types.insert(TimeInstants);
     types.insert(TimeValues);
     types.insert(Notes);
+    types.insert(Text);
     //!!! and in principle Colour3DPlot -- now that's a challenge
     return types;
 }
@@ -117,6 +125,7 @@
     if (dynamic_cast<const TimeInstantLayer *>(layer)) return TimeInstants;
     if (dynamic_cast<const TimeValueLayer *>(layer)) return TimeValues;
     if (dynamic_cast<const NoteLayer *>(layer)) return Notes;
+    if (dynamic_cast<const TextLayer *>(layer)) return Text;
     if (dynamic_cast<const Colour3DPlotLayer *>(layer)) return Colour3DPlot;
     return UnknownLayer;
 }
@@ -131,6 +140,7 @@
     case TimeInstants: return "instants";
     case TimeValues: return "values";
     case Notes: return "notes";
+    case Text: return "text";
     case Colour3DPlot: return "colour3d";
     default: return "unknown";
     }
@@ -146,6 +156,7 @@
     case TimeInstants: return "timeinstants";
     case TimeValues: return "timevalues";
     case Notes: return "notes";
+    case Text: return "text";
     case Colour3DPlot: return "colour3dplot";
     default: return "unknown";
     }
@@ -160,6 +171,7 @@
     if (name == "timeinstants") return TimeInstants;
     if (name == "timevalues") return TimeValues;
     if (name == "notes") return Notes;
+    if (name == "text") return Text;
     if (name == "colour3dplot") return Colour3DPlot;
     return UnknownLayer;
 }
@@ -185,6 +197,9 @@
     if (trySetModel<NoteLayer, NoteModel>(layer, model))
 	return;
 
+    if (trySetModel<TextLayer, TextModel>(layer, model))
+	return;
+
     if (trySetModel<Colour3DPlotLayer, DenseThreeDimensionalModel>(layer, model))
 	return;
 
@@ -203,6 +218,8 @@
     } else if (layerType == Notes) {
 	return new NoteModel(baseModel->getSampleRate(), 1,
 			     0.0, 0.0, true);
+    } else if (layerType == Text) {
+	return new TextModel(baseModel->getSampleRate(), 1, true);
     } else {
 	return 0;
     }
@@ -242,6 +259,10 @@
 	layer = new NoteLayer(view);
 	break;
 
+    case Text:
+	layer = new TextLayer(view);
+	break;
+
     case Colour3DPlot:
 	layer = new Colour3DPlotLayer(view);
 	break;
--- a/layer/LayerFactory.h	Fri Feb 17 18:11:08 2006 +0000
+++ b/layer/LayerFactory.h	Mon Feb 20 13:33:36 2006 +0000
@@ -29,6 +29,7 @@
 	TimeInstants,
 	TimeValues,
 	Notes,
+	Text,
 	Colour3DPlot,
 
 	// Layers with different initial parameters
--- a/layer/SpectrogramLayer.cpp	Fri Feb 17 18:11:08 2006 +0000
+++ b/layer/SpectrogramLayer.cpp	Mon Feb 20 13:33:36 2006 +0000
@@ -15,6 +15,8 @@
 #include "base/Window.h"
 #include "base/Pitch.h"
 
+#include "dsp/maths/MathUtilities.h"
+
 #include <QPainter>
 #include <QImage>
 #include <QPixmap>
@@ -42,7 +44,9 @@
     m_colourScale(dBColourScale),
     m_colourScheme(DefaultColours),
     m_frequencyScale(LinearFrequencyScale),
+    m_frequencyAdjustment(RawFrequency),
     m_cache(0),
+    m_phaseAdjustCache(0),
     m_cacheInvalid(true),
     m_pixmapCache(0),
     m_pixmapCacheInvalid(true),
@@ -74,6 +78,7 @@
     delete m_fillThread;
     
     delete m_cache;
+    delete m_phaseAdjustCache;
 }
 
 void
@@ -82,12 +87,15 @@
     std::cerr << "SpectrogramLayer(" << this << "): setModel(" << model << ")" << std::endl;
 
     m_mutex.lock();
+    m_cacheInvalid = true;
     m_model = model;
     delete m_cache; //!!! hang on, this isn't safe to do here is it? 
 		    // we need some sort of guard against the fill
 		    // thread trying to read the defunct model too.
 		    // should we use a scavenger?
     m_cache = 0;
+    delete m_phaseAdjustCache; //!!! likewise
+    m_phaseAdjustCache = 0;
     m_mutex.unlock();
 
     if (!m_model || !m_model->isOK()) return;
@@ -120,6 +128,7 @@
     list.push_back(tr("Colour Rotation"));
     list.push_back(tr("Max Frequency"));
     list.push_back(tr("Frequency Scale"));
+    list.push_back(tr("Frequency Adjustment"));
     return list;
 }
 
@@ -135,12 +144,15 @@
 SpectrogramLayer::getPropertyGroupName(const PropertyName &name) const
 {
     if (name == tr("Window Size") ||
+	name == tr("Window Type") ||
 	name == tr("Window Overlap")) return tr("Window");
+    if (name == tr("Colour") ||
+	name == tr("Colour Rotation")) return tr("Colour");
     if (name == tr("Gain") ||
-	name == tr("Colour Rotation") ||
 	name == tr("Colour Scale")) return tr("Scale");
     if (name == tr("Max Frequency") ||
-	name == tr("Frequency Scale")) return tr("Frequency");
+	name == tr("Frequency Scale") ||
+	name == tr("Frequency Adjustment")) return tr("Frequency");
     return QString();
 }
 
@@ -232,6 +244,12 @@
 	*max = 1;
 	deft = (int)m_frequencyScale;
 
+    } else if (name == tr("Frequency Adjustment")) {
+
+	*min = 0;
+	*max = 2;
+	deft = (int)m_frequencyAdjustment;
+
     } else {
 	deft = Layer::getPropertyRangeAndValue(name, min, max);
     }
@@ -266,7 +284,7 @@
     if (name == tr("Window Type")) {
 	switch ((WindowType)value) {
 	default:
-	case RectangularWindow: return tr("Rectangular");
+	case RectangularWindow: return tr("Rectangle");
 	case BartlettWindow: return tr("Bartlett");
 	case HammingWindow: return tr("Hamming");
 	case HanningWindow: return tr("Hanning");
@@ -281,11 +299,11 @@
     if (name == tr("Window Overlap")) {
 	switch (value) {
 	default:
-	case 0: return tr("None");
-	case 1: return tr("25 %");
-	case 2: return tr("50 %");
-	case 3: return tr("75 %");
-	case 4: return tr("90 %");
+	case 0: return tr("0%");
+	case 1: return tr("25%");
+	case 2: return tr("50%");
+	case 3: return tr("75%");
+	case 4: return tr("90%");
 	}
     }
     if (name == tr("Max Frequency")) {
@@ -310,6 +328,14 @@
 	case 1: return tr("Log");
 	}
     }
+    if (name == tr("Frequency Adjustment")) {
+	switch (value) {
+	default:
+	case 0: return tr("Bins");
+	case 1: return tr("Pitches");
+	case 2: return tr("Peaks");
+	}
+    }
     return tr("<unknown>");
 }
 
@@ -366,6 +392,13 @@
 	case 0: setFrequencyScale(LinearFrequencyScale); break;
 	case 1: setFrequencyScale(LogFrequencyScale); break;
 	}
+    } else if (name == tr("Frequency Adjustment")) {
+	switch (value) {
+	default:
+	case 0: setFrequencyAdjustment(RawFrequency); break;
+	case 1: setFrequencyAdjustment(PhaseAdjustedFrequency); break;
+	case 2: setFrequencyAdjustment(PhaseAdjustedPeaks); break;
+	}
     }
 }
 
@@ -584,6 +617,7 @@
     if (m_frequencyScale == frequencyScale) return;
 
     m_mutex.lock();
+
     // don't need to invalidate main cache here
     m_pixmapCacheInvalid = true;
     
@@ -601,6 +635,31 @@
 }
 
 void
+SpectrogramLayer::setFrequencyAdjustment(FrequencyAdjustment frequencyAdjustment)
+{
+    if (m_frequencyAdjustment == frequencyAdjustment) return;
+
+    m_mutex.lock();
+
+    m_cacheInvalid = true;
+    m_pixmapCacheInvalid = true;
+    
+    m_frequencyAdjustment = frequencyAdjustment;
+    
+    m_mutex.unlock();
+
+    fillCache();
+
+    emit layerParametersChanged();
+}
+
+SpectrogramLayer::FrequencyAdjustment
+SpectrogramLayer::getFrequencyAdjustment() const
+{
+    return m_frequencyAdjustment;
+}
+
+void
 SpectrogramLayer::setLayerDormant(bool dormant)
 {
     if (dormant == m_dormant) return;
@@ -718,9 +777,6 @@
 
     int formerRotation = m_colourRotation;
 
-//    m_cache->setNumColors(256);
-    
-//    m_cache->setColour(0, qRgb(255, 255, 255));
     m_cache->setColour(0, Qt::white);
 
     for (int pixel = 1; pixel < 256; ++pixel) {
@@ -764,8 +820,6 @@
 	    break;
 	}
 
-//	m_cache->setColor
-//	    (pixel, qRgb(colour.red(), colour.green(), colour.blue()));
 	m_cache->setColour(pixel, colour);
     }
 
@@ -802,8 +856,31 @@
 				  size_t windowSize,
 				  size_t increment,
 				  const Window<double> &windower,
-				  bool lock) const
+				  bool resetStoredPhase) const
 {
+    static std::vector<double> storedPhase;
+
+    bool phaseAdjust =
+	(m_frequencyAdjustment == PhaseAdjustedFrequency ||
+	 m_frequencyAdjustment == PhaseAdjustedPeaks);
+    bool haveStoredPhase = true;
+    size_t sampleRate = 0;
+
+    static int counter = 0;
+
+    if (phaseAdjust) {
+	if (resetStoredPhase || (storedPhase.size() != windowSize / 2)) {
+	    haveStoredPhase = false;
+	    storedPhase.clear();
+	    for (size_t i = 0; i < windowSize / 2; ++i) {
+		storedPhase.push_back(0.0);
+	    }
+	    counter = 0;
+	}
+	++counter;
+	sampleRate = m_model->getSampleRate();
+    }
+
     int startFrame = increment * column;
     int endFrame = startFrame + windowSize;
 
@@ -833,18 +910,112 @@
 
     windower.cut(input);
 
+    for (size_t i = 0; i < windowSize/2; ++i) {
+	double temp = input[i];
+	input[i] = input[i + windowSize/2];
+	input[i + windowSize/2] = temp;
+    }
+    
     fftw_execute(plan);
 
-//    if (lock) m_mutex.lock();
     bool interrupted = false;
 
+    double prevMag = 0.0;
+
     for (size_t i = 0; i < windowSize / 2; ++i) {
 
 	int value = 0;
+	double phase = 0.0;
+
+	    double mag = sqrt(output[i][0] * output[i][0] +
+			      output[i][1] * output[i][1]);
+	    mag /= windowSize / 2;
+
+	if (phaseAdjust || (m_colourScale == PhaseColourScale)) {
+
+//	    phase = atan2(-output[i][1], output[i][0]);
+//	    phase = atan2(output[i][1], output[i][0]);
+	    phase = atan2(output[i][0], output[i][1]);
+//	    phase = MathUtilities::princarg(phase);
+	}	    
+
+	if (phaseAdjust && m_phaseAdjustCache && haveStoredPhase) {
+
+	    bool peak = true;
+	    if (m_frequencyAdjustment == PhaseAdjustedPeaks) {
+		if (mag < prevMag) peak = false;
+		else {
+		    double nextMag = 0.0;
+		    if (i < windowSize / 2 - 1) {
+			nextMag = sqrt(output[i+1][0] * output[i+1][0] +
+				       output[i+1][1] * output[i+1][1]);
+			nextMag /= windowSize / 2;
+		    }
+		    if (mag < nextMag) peak = false;
+		}
+		prevMag = mag;
+	    }
+
+	    if (!peak) {
+		if (m_cacheInvalid || m_exiting) {
+		    interrupted = true;
+		    break;
+		}
+		m_phaseAdjustCache->setValueAt(column, i, SCHAR_MIN);
+	    } else {
+
+//	    if (i > 45 && i < 55 && counter == 10)  {
+	    
+	    double freq = (double(i) * sampleRate) / m_windowSize;
+//	    std::cout << "\nbin = " << i << " initial estimate freq = " << freq
+//		      << " mag = " << mag << std::endl;
+
+	    double prevPhase = storedPhase[i];
+
+	    double expectedPhase =
+		prevPhase + (2 * M_PI * i * increment) / m_windowSize;
+
+	    double phaseError = MathUtilities::princarg(phase - expectedPhase);
+	    
+//	    if (fabs(phaseError) > (1.2 * (increment * M_PI) / m_windowSize)) {
+//		std::cout << "error > " << (1.2 * (increment * M_PI) / m_windowSize) << std::endl;
+//	    }// else {
+
+//	    std::cout << "prevPhase = " << prevPhase << ", phase = " << phase
+//		      << ", expected = " << MathUtilities::princarg(expectedPhase) << ", error = "
+//		      << phaseError << std::endl;
+
+	    double newFreq =
+		(sampleRate *
+		 (expectedPhase + phaseError - prevPhase)) /
+		//(prevPhase - (expectedPhase + phaseError))) /
+		(2 * M_PI * increment);
+
+//	    std::cout << freq << " (" << Pitch::getPitchLabelForFrequency(freq).toStdString() <<  ") -> " << newFreq << " (" << Pitch::getPitchLabelForFrequency(newFreq).toStdString() << ")" << std::endl;
+//	    }
+//}
+
+	    double binRange = (double(i + 1) * sampleRate) / windowSize - freq;
+	    
+	    int offset = lrint(((newFreq - freq) / binRange) * 10);//!!!
+
+	    if (m_cacheInvalid || m_exiting) {
+		interrupted = true;
+		break;
+	    }
+	    if (offset > SCHAR_MIN && offset <= SCHAR_MAX) {
+		signed char coff = offset;
+		m_phaseAdjustCache->setValueAt(column, i, (unsigned char)coff);
+	    } else {
+		m_phaseAdjustCache->setValueAt(column, i, 0);
+	    }
+	    }
+	    storedPhase[i] = phase;
+	}
 
 	if (m_colourScale == PhaseColourScale) {
 
-	    double phase = atan2(-output[i][1], output[i][0]);
+	    phase = MathUtilities::princarg(phase);
 	    value = int((phase * 128 / M_PI) + 128);
 
 	} else {
@@ -884,7 +1055,6 @@
 	m_cache->setValueAt(column, i, value + 1);
     }
 
-//    if (lock) m_mutex.unlock();
     return !interrupted;
 }
 
@@ -892,15 +1062,27 @@
     m_width(width),
     m_height(height)
 {
-    m_values = new unsigned char[m_width * m_height];
+    // use malloc rather than new[], because we want to be able to use realloc
+    m_values = (unsigned char *)
+	malloc(m_width * m_height * sizeof(unsigned char));
+    if (!m_values) throw std::bad_alloc();
     MUNLOCK(m_values, m_width * m_height * sizeof(unsigned char));
 }
 
 SpectrogramLayer::Cache::~Cache()
 {
-    delete[] m_values;
+    if (m_values) free(m_values);
 }
 
+void
+SpectrogramLayer::Cache::resize(size_t width, size_t height)
+{
+    m_values = (unsigned char *)
+	realloc(m_values, m_width * m_height * sizeof(unsigned char));
+    if (!m_values) throw std::bad_alloc();
+    MUNLOCK(m_values, m_width * m_height * sizeof(unsigned char));
+}    
+
 size_t
 SpectrogramLayer::Cache::getWidth() const
 {
@@ -964,7 +1146,9 @@
 
 	    if (m_layer.m_cacheInvalid) {
 		delete m_layer.m_cache;
+		delete m_layer.m_phaseAdjustCache;
 		m_layer.m_cache = 0;
+		m_layer.m_phaseAdjustCache = 0;
 	    }
 
 	} else if (m_layer.m_model && m_layer.m_cacheInvalid) {
@@ -1003,18 +1187,40 @@
 		visibleEnd = m_layer.m_view->getEndFrame();
 	    }
 
-	    delete m_layer.m_cache;
 	    size_t width = (end - start) / windowIncrement + 1;
 	    size_t height = windowSize / 2;
-	    m_layer.m_cache = new Cache(width, height);
+
+	    if (!m_layer.m_cache) {
+		m_layer.m_cache = new Cache(width, height);
+	    } else if (width != m_layer.m_cache->getWidth() ||
+		       height != m_layer.m_cache->getHeight()) {
+		m_layer.m_cache->resize(width, height);
+	    }
 
 	    m_layer.setCacheColourmap();
 	    m_layer.m_cache->fill(0);
 
+	    if (m_layer.m_frequencyAdjustment == PhaseAdjustedFrequency ||
+		m_layer.m_frequencyAdjustment == PhaseAdjustedPeaks) {
+		
+		if (!m_layer.m_phaseAdjustCache) {
+		    m_layer.m_phaseAdjustCache = new Cache(width, height);
+		} else if (width != m_layer.m_phaseAdjustCache->getWidth() ||
+			   height != m_layer.m_phaseAdjustCache->getHeight()) {
+		    m_layer.m_phaseAdjustCache->resize(width, height);
+		}
+
+		m_layer.m_phaseAdjustCache->fill(0);
+
+	    } else {
+		delete m_layer.m_phaseAdjustCache;
+		m_layer.m_phaseAdjustCache = 0;
+	    }
+
 	    // We don't need a lock when writing to or reading from
 	    // the pixels in the cache, because it's a fixed size
 	    // array.  We do need to ensure we have the width and
-	    // height of the cache and the FFT parameters fixed before
+	    // height of the cache and the FFT parameters known before
 	    // we unlock, in case they change in the model while we
 	    // aren't holding a lock.  It's safe for us to continue to
 	    // use the "old" values if that happens, because they will
@@ -1053,7 +1259,8 @@
 		    m_layer.fillCacheColumn(int((f - start) / windowIncrement),
 					    input, output, plan,
 					    windowSize, windowIncrement,
-					    windower, false);
+					    //!!! actually if we're doing phase adjustment we also want to fill the column preceding the visible area so that we have the right values for the first visible one (also applies below)
+					    windower, f == visibleStart);
 
 		    if (m_layer.m_cacheInvalid || m_layer.m_exiting) {
 			interrupted = true;
@@ -1079,7 +1286,7 @@
 		    if (!m_layer.fillCacheColumn(int((f - start) / windowIncrement),
 						 input, output, plan,
 						 windowSize, windowIncrement,
-						 windower, true)) {
+						 windower, f == visibleEnd)) {
 			interrupted = true;
 			m_fillExtent = 0;
 			break;
@@ -1110,7 +1317,7 @@
 		    if (!m_layer.fillCacheColumn(int((f - start) / windowIncrement),
 						 input, output, plan,
 						 windowSize, windowIncrement,
-						 windower, true)) {
+						 windower, f == start)) {
 			interrupted = true;
 			m_fillExtent = 0;
 			break;
@@ -1177,7 +1384,7 @@
 	
 	float maxlogf = log10f(maxf);
 	float minlogf = log10f(minf);
-
+ 
 	float logf0 = minlogf + ((maxlogf - minlogf) * (h - y - 1)) / h;
 	float logf1 = minlogf + ((maxlogf - minlogf) * (h - y)) / h;
 	
@@ -1249,12 +1456,70 @@
     int sr = m_model->getSampleRate();
 
     for (int q = q0i; q <= q1i; ++q) {
-	int binfreq = (sr * (q + 1)) / m_windowSize;
+	int binfreq = (sr * q) / m_windowSize;
 	if (q == q0i) freqMin = binfreq;
 	if (q == q1i) freqMax = binfreq;
     }
     return true;
 }
+
+bool
+SpectrogramLayer::getAdjustedYBinSourceRange(int x, int y,
+					     float &freqMin, float &freqMax,
+					     float &adjFreqMin, float &adjFreqMax)
+const
+{
+    float s0 = 0, s1 = 0;
+    if (!getXBinRange(x, s0, s1)) return false;
+
+    float q0 = 0, q1 = 0;
+    if (!getYBinRange(y, q0, q1)) return false;
+
+    int s0i = int(s0 + 0.001);
+    int s1i = int(s1);
+
+    int q0i = int(q0 + 0.001);
+    int q1i = int(q1);
+
+    int sr = m_model->getSampleRate();
+
+    bool haveAdj = false;
+
+    for (int q = q0i; q <= q1i; ++q) {
+
+	for (int s = s0i; s <= s1i; ++s) {
+
+	    float binfreq = (sr * q) / m_windowSize;
+	    if (q == q0i) freqMin = binfreq;
+	    if (q == q1i) freqMax = binfreq;
+	    
+	    if ((m_frequencyAdjustment == PhaseAdjustedFrequency ||
+		 m_frequencyAdjustment == PhaseAdjustedPeaks) &&
+		m_phaseAdjustCache) {
+
+		unsigned char cadj = m_phaseAdjustCache->getValueAt(s, q);
+		int adjust = int((signed char)cadj);
+		if (adjust == SCHAR_MIN &&
+		    m_frequencyAdjustment == PhaseAdjustedPeaks) {
+		    continue;
+		}
+		
+		float nextBinFreq = (sr * (q + 1)) / m_windowSize;
+		float fadjust = (adjust * (nextBinFreq - binfreq)) / 10.0;//!!!
+		float f = binfreq + fadjust;
+		if (!haveAdj || f < adjFreqMin) adjFreqMin = f;
+		if (!haveAdj || f > adjFreqMax) adjFreqMax = f;
+		haveAdj = true;
+	    }
+	}
+    }
+
+    if (!haveAdj) {
+	adjFreqMin = adjFreqMax = 0.0f;
+    }
+
+    return haveAdj;
+}
     
 bool
 SpectrogramLayer::getXYBinSourceRange(int x, int y, float &dbMin, float &dbMax) const
@@ -1462,9 +1727,132 @@
 //    std::cerr << "x0 " << x0 << ", x1 " << x1 << ", w " << w << ", h " << h << std::endl;
 
     QImage scaled(w, h, QImage::Format_RGB32);
+    scaled.fill(0);
+
+    float ymag[h];
+    float ydiv[h];
+    
+    size_t bins = m_windowSize / 2;
+    int sr = m_model->getSampleRate();
+    
+    if (m_maxFrequency > 0) {
+	bins = int((double(m_maxFrequency) * m_windowSize) / sr + 0.1);
+	if (bins > m_windowSize / 2) bins = m_windowSize / 2;
+    }
+	
+    float maxFreq = (float(bins) * sr) / m_windowSize;
 
     m_mutex.unlock();
 
+    for (int x = 0; x < w; ++x) {
+
+	m_mutex.lock();
+	if (m_cacheInvalid) {
+	    m_mutex.unlock();
+	    break;
+	}
+
+	for (int y = 0; y < h; ++y) {
+	    ymag[y] = 0.0f;
+	    ydiv[y] = 0.0f;
+	}
+
+	float s0 = 0, s1 = 0;
+
+	if (!getXBinRange(x0 + x, s0, s1)) {
+	    assert(x <= scaled.width());
+	    for (int y = 0; y < h; ++y) {
+		scaled.setPixel(x, y, qRgb(0, 0, 0));
+	    }
+	    m_mutex.unlock();
+	    continue;
+	}
+
+	int s0i = int(s0 + 0.001);
+	int s1i = int(s1);
+
+	for (int q = 0; q < bins; ++q) {
+
+	    for (int s = s0i; s <= s1i; ++s) {
+
+		float sprop = 1.0;
+		if (s == s0i) sprop *= (s + 1) - s0;
+		if (s == s1i) sprop *= s1 - s;
+
+		float f0 = (float(q) * sr) / m_windowSize;
+		float f1 = (float(q + 1) * sr) / m_windowSize;
+ 
+		if ((m_frequencyAdjustment == PhaseAdjustedFrequency ||
+		     m_frequencyAdjustment == PhaseAdjustedPeaks) &&
+		    m_phaseAdjustCache) {
+
+		    unsigned char cadj = m_phaseAdjustCache->getValueAt(s, q);
+		    int adjust = int((signed char)cadj);
+
+		    if (adjust == SCHAR_MIN &&
+			m_frequencyAdjustment == PhaseAdjustedPeaks) {
+			continue;
+		    }
+
+		    float fadjust = (adjust * (f1 - f0)) / 10.0;//!!! was 100
+		    f0 = f1 = f0 + fadjust;
+		}
+	    
+		float y0 = h - (h * f1) / maxFreq;
+		float y1 = h - (h * f0) / maxFreq;
+
+		if (m_frequencyScale == LogFrequencyScale) {
+		    
+		    float maxf = m_maxFrequency;
+		    if (maxf == 0.0) maxf = float(sr) / 2;
+		    
+		    float minf = float(sr) / m_windowSize;
+		    
+		    float maxlogf = log10f(maxf);
+		    float minlogf = log10f(minf);
+		    
+		    y0 = h - (h * (log10f(f1) - minlogf)) / (maxlogf - minlogf);
+		    y1 = h - (h * (log10f(f0) - minlogf)) / (maxlogf - minlogf);
+		}
+
+		int y0i = int(y0 + 0.001);
+		int y1i = int(y1);
+
+		for (int y = y0i; y <= y1i; ++y) {
+		    
+		    if (y < 0 || y >= h) continue;
+
+		    float yprop = sprop;
+		    if (y == y0i) yprop *= (y + 1) - y0;
+		    if (y == y1i) yprop *= y1 - y;
+		    
+		    ymag[y] += yprop * m_cache->getValueAt(s, q);
+		    ydiv[y] += yprop;
+		}
+	    }
+	}
+
+	for (int y = 0; y < h; ++y) {
+
+	    int pixel = 1;
+
+	    if (ydiv[y] > 0.0) {
+		pixel = int(ymag[y] / ydiv[y]);
+		if (pixel > 255) pixel = 255;
+		if (pixel < 1) pixel = 1;
+	    }
+
+	    assert(x <= scaled.width());
+	    QColor c = m_cache->getColour(pixel);
+	    scaled.setPixel(x, y,
+			    qRgb(c.red(), c.green(), c.blue()));
+	}
+    
+
+	m_mutex.unlock();
+    }
+
+#ifdef NOT_DEFINED
     for (int y = 0; y < h; ++y) {
 
 	m_mutex.lock();
@@ -1553,6 +1941,7 @@
 
 	m_mutex.unlock();
     }
+#endif
 
     paint.drawImage(x0, y0, scaled);
 
@@ -1615,15 +2004,42 @@
 
     float dbMin = 0, dbMax = 0;
     float freqMin = 0, freqMax = 0;
+    float adjFreqMin = 0, adjFreqMax = 0;
     QString pitchMin, pitchMax;
     RealTime rtMin, rtMax;
 
     bool haveDb = false;
 
     if (!getXBinSourceRange(x, rtMin, rtMax)) return "";
-    if (!getYBinSourceRange(y, freqMin, freqMax)) return "";
     if (getXYBinSourceRange(x, y, dbMin, dbMax)) haveDb = true;
 
+    QString adjFreqText = "", adjPitchText = "";
+
+    if ((m_frequencyAdjustment == PhaseAdjustedFrequency ||
+	 m_frequencyAdjustment == PhaseAdjustedPeaks) &&
+	m_phaseAdjustCache) {
+
+	if (!getAdjustedYBinSourceRange(x, y, freqMin, freqMax,
+					adjFreqMin, adjFreqMax)) return "";
+
+	if (adjFreqMin != adjFreqMax) {
+	    adjFreqText = tr("Adjusted Frequency:\t%1 - %2 Hz\n")
+		.arg(adjFreqMin).arg(adjFreqMax);
+	    adjPitchText = tr("Adjusted Pitch:\t%3 - %4\n")
+		.arg(Pitch::getPitchLabelForFrequency(adjFreqMin))
+		.arg(Pitch::getPitchLabelForFrequency(adjFreqMax));
+	} else {
+	    adjFreqText = tr("Adjusted Frequency:\t%1 Hz\n")
+		.arg(adjFreqMin);
+	    adjPitchText = tr("Adjusted Pitch:\t%2\n")
+		.arg(Pitch::getPitchLabelForFrequency(adjFreqMin));
+	}
+
+    } else {
+	
+	if (!getYBinSourceRange(y, freqMin, freqMax)) return "";
+    }
+
     //!!! want to actually do a one-off FFT to recalculate the dB value!
 
     QString text;
@@ -1638,15 +2054,19 @@
     }
 
     if (freqMin != freqMax) {
-	text += tr("Frequency:\t%1 - %2 Hz\nPitch:\t%3 - %4\n")
+	text += tr("Frequency:\t%1 - %2 Hz\n%3Pitch:\t%4 - %5\n%6")
 	    .arg(freqMin)
 	    .arg(freqMax)
+	    .arg(adjFreqText)
 	    .arg(Pitch::getPitchLabelForFrequency(freqMin))
-	    .arg(Pitch::getPitchLabelForFrequency(freqMax));
+	    .arg(Pitch::getPitchLabelForFrequency(freqMax))
+	    .arg(adjPitchText);
     } else {
-	text += tr("Frequency:\t%1 Hz\nPitch:\t%2\n")
+	text += tr("Frequency:\t%1 Hz\n%2Pitch:\t%3\n%4")
 	    .arg(freqMin)
-	    .arg(Pitch::getPitchLabelForFrequency(freqMin));
+	    .arg(adjFreqText)
+	    .arg(Pitch::getPitchLabelForFrequency(freqMin))
+	    .arg(adjPitchText);
     }	
 
     if (haveDb) {
@@ -1742,20 +2162,23 @@
 		 "windowSize=\"%2\" "
 		 "windowType=\"%3\" "
 		 "windowOverlap=\"%4\" "
-		 "gain=\"%5\" "
-		 "maxFrequency=\"%6\" "
-		 "colourScale=\"%7\" "
-		 "colourScheme=\"%8\" "
-		 "frequencyScale=\"%9\"")
+		 "gain=\"%5\" ")
 	.arg(m_channel)
 	.arg(m_windowSize)
 	.arg(m_windowType)
 	.arg(m_windowOverlap)
-	.arg(m_gain)
+	.arg(m_gain);
+
+    s += QString("maxFrequency=\"%1\" "
+		 "colourScale=\"%2\" "
+		 "colourScheme=\"%3\" "
+		 "frequencyScale=\"%4\" "
+		 "frequencyAdjustment=\"%5\"")
 	.arg(m_maxFrequency)
 	.arg(m_colourScale)
 	.arg(m_colourScheme)
-	.arg(m_frequencyScale);
+	.arg(m_frequencyScale)
+	.arg(m_frequencyAdjustment);
 
     return Layer::toXmlString(indent, extraAttributes + " " + s);
 }
@@ -1795,6 +2218,10 @@
     FrequencyScale frequencyScale = (FrequencyScale)
 	attributes.value("frequencyScale").toInt(&ok);
     if (ok) setFrequencyScale(frequencyScale);
+
+    FrequencyAdjustment frequencyAdjustment = (FrequencyAdjustment)
+	attributes.value("frequencyAdjustment").toInt(&ok);
+    if (ok) setFrequencyAdjustment(frequencyAdjustment);
 }
     
 
--- a/layer/SpectrogramLayer.h	Fri Feb 17 18:11:08 2006 +0000
+++ b/layer/SpectrogramLayer.h	Mon Feb 20 13:33:36 2006 +0000
@@ -107,7 +107,10 @@
     void setColourScale(ColourScale);
     ColourScale getColourScale() const;
 
-    enum FrequencyScale { LinearFrequencyScale, LogFrequencyScale };
+    enum FrequencyScale {
+	LinearFrequencyScale,
+	LogFrequencyScale
+    };
     
     /**
      * Specify the scale for the y axis.
@@ -115,6 +118,18 @@
     void setFrequencyScale(FrequencyScale);
     FrequencyScale getFrequencyScale() const;
 
+    enum FrequencyAdjustment {
+	RawFrequency,
+	PhaseAdjustedFrequency,
+	PhaseAdjustedPeaks
+    };
+    
+    /**
+     * Specify the processing of frequency bins for the y axis.
+     */
+    void setFrequencyAdjustment(FrequencyAdjustment);
+    FrequencyAdjustment getFrequencyAdjustment() const;
+
     enum ColourScheme { DefaultColours, WhiteOnBlack, BlackOnWhite,
 			RedOnBlue, YellowOnBlack, RedOnBlack };
 
@@ -151,16 +166,17 @@
 protected:
     const DenseTimeValueModel *m_model; // I do not own this
     
-    int            m_channel;
-    size_t         m_windowSize;
-    WindowType     m_windowType;
-    size_t         m_windowOverlap;
-    float          m_gain;
-    int            m_colourRotation;
-    size_t         m_maxFrequency;
-    ColourScale    m_colourScale;
-    ColourScheme   m_colourScheme;
-    FrequencyScale m_frequencyScale;
+    int                 m_channel;
+    size_t              m_windowSize;
+    WindowType          m_windowType;
+    size_t              m_windowOverlap;
+    float               m_gain;
+    int                 m_colourRotation;
+    size_t              m_maxFrequency;
+    ColourScale         m_colourScale;
+    ColourScheme        m_colourScheme;
+    FrequencyScale      m_frequencyScale;
+    FrequencyAdjustment m_frequencyAdjustment;
 
     // A QImage would do just as well here, and we originally used
     // one: the problem is that we want to munlock() the memory it
@@ -173,6 +189,8 @@
 
 	size_t getWidth() const;
 	size_t getHeight() const;
+
+	void resize(size_t width, size_t height);
 	
 	unsigned char getValueAt(size_t x, size_t y) const;
 	void setValueAt(size_t x, size_t y, unsigned char value);
@@ -190,6 +208,7 @@
     };
     
     Cache *m_cache;
+    Cache *m_phaseAdjustCache;
     bool m_cacheInvalid;
 
     class CacheFillThread : public QThread
@@ -234,7 +253,7 @@
 			 size_t windowSize,
 			 size_t windowIncrement,
 			 const Window<double> &windower,
-			 bool lock)
+			 bool resetStoredPhase)
 	const;
 
     bool getYBinRange(int y, float &freqBinMin, float &freqBinMax) const;
@@ -248,6 +267,9 @@
     bool getXBinRange(int x, float &windowMin, float &windowMax) const;
 
     bool getYBinSourceRange(int y, float &freqMin, float &freqMax) const;
+    bool getAdjustedYBinSourceRange(int x, int y,
+				    float &freqMin, float &freqMax,
+				    float &adjFreqMin, float &adjFreqMax) const;
     bool getXBinSourceRange(int x, RealTime &timeMin, RealTime &timeMax) const;
     bool getXYBinSourceRange(int x, int y, float &dbMin, float &dbMax) const;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/TextLayer.cpp	Mon Feb 20 13:33:36 2006 +0000
@@ -0,0 +1,577 @@
+/* -*- c-basic-offset: 4 -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    A waveform viewer and audio annotation editor.
+    Chris Cannam, Queen Mary University of London, 2005-2006
+    
+    This is experimental software.  Not for distribution.
+*/
+
+#include "TextLayer.h"
+
+#include "base/Model.h"
+#include "base/RealTime.h"
+#include "base/Profiler.h"
+#include "base/View.h"
+
+#include "model/TextModel.h"
+
+#include <QPainter>
+#include <QMouseEvent>
+
+#include <iostream>
+#include <cmath>
+
+TextLayer::TextLayer(View *w) :
+    Layer(w),
+    m_model(0),
+    m_editing(false),
+    m_originalPoint(0, 0.0, tr("Empty Label")),
+    m_editingPoint(0, 0.0, tr("Empty Label")),
+    m_editingCommand(0),
+    m_colour(255, 150, 50) // orange
+{
+    m_view->addLayer(this);
+}
+
+void
+TextLayer::setModel(TextModel *model)
+{
+    if (m_model == model) return;
+    m_model = model;
+
+    connect(m_model, SIGNAL(modelChanged()), this, SIGNAL(modelChanged()));
+    connect(m_model, SIGNAL(modelChanged(size_t, size_t)),
+	    this, SIGNAL(modelChanged(size_t, size_t)));
+
+    connect(m_model, SIGNAL(completionChanged()),
+	    this, SIGNAL(modelCompletionChanged()));
+
+    std::cerr << "TextLayer::setModel(" << model << ")" << std::endl;
+
+    emit modelReplaced();
+}
+
+Layer::PropertyList
+TextLayer::getProperties() const
+{
+    PropertyList list;
+    list.push_back(tr("Colour"));
+    return list;
+}
+
+Layer::PropertyType
+TextLayer::getPropertyType(const PropertyName &name) const
+{
+    return ValueProperty;
+}
+
+int
+TextLayer::getPropertyRangeAndValue(const PropertyName &name,
+				    int *min, int *max) const
+{
+    //!!! factor this colour handling stuff out into a colour manager class
+
+    int deft = 0;
+
+    if (name == tr("Colour")) {
+
+	if (min) *min = 0;
+	if (max) *max = 5;
+
+	if (m_colour == Qt::black) deft = 0;
+	else if (m_colour == Qt::darkRed) deft = 1;
+	else if (m_colour == Qt::darkBlue) deft = 2;
+	else if (m_colour == Qt::darkGreen) deft = 3;
+	else if (m_colour == QColor(200, 50, 255)) deft = 4;
+	else if (m_colour == QColor(255, 150, 50)) deft = 5;
+
+    } else {
+	
+	deft = Layer::getPropertyRangeAndValue(name, min, max);
+    }
+
+    return deft;
+}
+
+QString
+TextLayer::getPropertyValueLabel(const PropertyName &name,
+				 int value) const
+{
+    if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0: return tr("Black");
+	case 1: return tr("Red");
+	case 2: return tr("Blue");
+	case 3: return tr("Green");
+	case 4: return tr("Purple");
+	case 5: return tr("Orange");
+	}
+    }
+    return tr("<unknown>");
+}
+
+void
+TextLayer::setProperty(const PropertyName &name, int value)
+{
+    if (name == tr("Colour")) {
+	switch (value) {
+	default:
+	case 0:	setBaseColour(Qt::black); break;
+	case 1: setBaseColour(Qt::darkRed); break;
+	case 2: setBaseColour(Qt::darkBlue); break;
+	case 3: setBaseColour(Qt::darkGreen); break;
+	case 4: setBaseColour(QColor(200, 50, 255)); break;
+	case 5: setBaseColour(QColor(255, 150, 50)); break;
+	}
+    }
+}
+
+void
+TextLayer::setBaseColour(QColor colour)
+{
+    if (m_colour == colour) return;
+    m_colour = colour;
+    emit layerParametersChanged();
+}
+
+bool
+TextLayer::isLayerScrollable() const
+{
+    QPoint discard;
+    return !m_view->shouldIlluminateLocalFeatures(this, discard);
+}
+
+
+TextModel::PointList
+TextLayer::getLocalPoints(int x, int y) const
+{
+    if (!m_model) return TextModel::PointList();
+
+    long frame0 = getFrameForX(-150);
+    long frame1 = getFrameForX(m_view->width() + 150);
+    
+    TextModel::PointList points(m_model->getPoints(frame0, frame1));
+
+    TextModel::PointList rv;
+    QFontMetrics metrics = QPainter().fontMetrics();
+
+    for (TextModel::PointList::iterator i = points.begin();
+	 i != points.end(); ++i) {
+
+	const TextModel::Point &p(*i);
+
+	int px = getXForFrame(p.frame);
+	int py = getYForHeight(p.height);
+
+	QString label = p.label;
+	if (label == "") {
+	    label = tr("<no text>");
+	}
+
+	QRect rect = metrics.boundingRect
+	    (QRect(0, 0, 150, 200),
+	     Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label);
+
+	if (py + rect.height() > m_view->height()) {
+	    if (rect.height() > m_view->height()) py = 0;
+	    else py = m_view->height() - rect.height() - 1;
+	}
+
+	if (x >= px && x < px + rect.width() &&
+	    y >= py && y < py + rect.height()) {
+	    rv.insert(p);
+	}
+    }
+
+    return rv;
+}
+
+QString
+TextLayer::getFeatureDescription(QPoint &pos) const
+{
+    int x = pos.x();
+
+    if (!m_model || !m_model->getSampleRate()) return "";
+
+    TextModel::PointList points = getLocalPoints(x, pos.y());
+
+    if (points.empty()) {
+	if (!m_model->isReady()) {
+	    return tr("In progress");
+	} else {
+	    return "";
+	}
+    }
+
+    long useFrame = points.begin()->frame;
+
+    RealTime rt = RealTime::frame2RealTime(useFrame, m_model->getSampleRate());
+    
+    QString text;
+
+    if (points.begin()->label == "") {
+	text = QString(tr("Time:\t%1\nHeight:\t%2\nLabel:\t%3"))
+	    .arg(rt.toText(true).c_str())
+	    .arg(points.begin()->height)
+	    .arg(points.begin()->label);
+    }
+
+    pos = QPoint(getXForFrame(useFrame), getYForHeight(points.begin()->height));
+    return text;
+}
+
+
+//!!! too much overlap with TimeValueLayer/TimeInstantLayer
+
+bool
+TextLayer::snapToFeatureFrame(int &frame,
+			      size_t &resolution,
+			      SnapType snap) const
+{
+    if (!m_model) {
+	return Layer::snapToFeatureFrame(frame, resolution, snap);
+    }
+
+    resolution = m_model->getResolution();
+    TextModel::PointList points;
+
+    if (snap == SnapNeighbouring) {
+	
+	points = getLocalPoints(getXForFrame(frame), -1);
+	if (points.empty()) return false;
+	frame = points.begin()->frame;
+	return true;
+    }    
+
+    points = m_model->getPoints(frame, frame);
+    int snapped = frame;
+    bool found = false;
+
+    for (TextModel::PointList::const_iterator i = points.begin();
+	 i != points.end(); ++i) {
+
+	if (snap == SnapRight) {
+
+	    if (i->frame > frame) {
+		snapped = i->frame;
+		found = true;
+		break;
+	    }
+
+	} else if (snap == SnapLeft) {
+
+	    if (i->frame <= frame) {
+		snapped = i->frame;
+		found = true; // don't break, as the next may be better
+	    } else {
+		break;
+	    }
+
+	} else { // nearest
+
+	    TextModel::PointList::const_iterator j = i;
+	    ++j;
+
+	    if (j == points.end()) {
+
+		snapped = i->frame;
+		found = true;
+		break;
+
+	    } else if (j->frame >= frame) {
+
+		if (j->frame - frame < frame - i->frame) {
+		    snapped = j->frame;
+		} else {
+		    snapped = i->frame;
+		}
+		found = true;
+		break;
+	    }
+	}
+    }
+
+    frame = snapped;
+    return found;
+}
+
+int
+TextLayer::getYForHeight(float height) const
+{
+    int h = m_view->height();
+    return h - int(height * h);
+}
+
+float
+TextLayer::getHeightForY(int y) const
+{
+    int h = m_view->height();
+    return float(h - y) / h;
+}
+
+void
+TextLayer::paint(QPainter &paint, QRect rect) const
+{
+    if (!m_model || !m_model->isOK()) return;
+
+    int sampleRate = m_model->getSampleRate();
+    if (!sampleRate) return;
+
+//    Profiler profiler("TextLayer::paint", true);
+
+    int x0 = rect.left(), x1 = rect.right();
+    long frame0 = getFrameForX(x0);
+    long frame1 = getFrameForX(x1);
+
+    TextModel::PointList points(m_model->getPoints(frame0, frame1));
+    if (points.empty()) return;
+
+    QColor brushColour(m_colour);
+    brushColour.setAlpha(80);
+    paint.setBrush(brushColour);
+
+    if (m_view->hasLightBackground()) {
+	paint.setPen(Qt::black);
+    } else {
+	paint.setPen(Qt::white);
+    }
+
+//    std::cerr << "TextLayer::paint: resolution is "
+//	      << m_model->getResolution() << " frames" << std::endl;
+
+    QPoint localPos;
+    long illuminateFrame = -1;
+
+    if (m_view->shouldIlluminateLocalFeatures(this, localPos)) {
+	TextModel::PointList localPoints = getLocalPoints(localPos.x(),
+							  localPos.y());
+	if (!localPoints.empty()) illuminateFrame = localPoints.begin()->frame;
+    }
+
+    int boxMaxWidth = 150;
+    int boxMaxHeight = 200;
+
+    paint.save();
+    paint.setClipRect(rect.x(), 0, rect.width() + boxMaxWidth, m_view->height());
+    
+    for (TextModel::PointList::const_iterator i = points.begin();
+	 i != points.end(); ++i) {
+
+	const TextModel::Point &p(*i);
+
+	int x = getXForFrame(p.frame);
+	int y = getYForHeight(p.height);
+
+	if (illuminateFrame == p.frame) {
+/*
+	    //!!! aside from the problem of choosing a colour, it'd be
+	    //better to save the highlighted rects and draw them at
+	    //the end perhaps
+
+	    //!!! not equipped to illuminate the right section in line
+	    //or curve mode
+
+	    if (m_plotStyle != PlotCurve &&
+		m_plotStyle != PlotLines) {
+		paint.setPen(Qt::black);//!!!
+		if (m_plotStyle != PlotSegmentation) {
+		    paint.setBrush(Qt::black);//!!!
+		}
+	    }	 
+*/   
+	}
+
+	QString label = p.label;
+	if (label == "") {
+	    label = tr("<no text>");
+	}
+
+	QRect boxRect = paint.fontMetrics().boundingRect
+	    (QRect(0, 0, boxMaxWidth, boxMaxHeight),
+	     Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, label);
+
+	QRect textRect = QRect(3, 2, boxRect.width(), boxRect.height());
+	boxRect = QRect(0, 0, boxRect.width() + 6, boxRect.height() + 2);
+
+	if (y + boxRect.height() > m_view->height()) {
+	    if (boxRect.height() > m_view->height()) y = 0;
+	    else y = m_view->height() - boxRect.height() - 1;
+	}
+
+	boxRect = QRect(x, y, boxRect.width(), boxRect.height());
+	textRect = QRect(x + 3, y + 2, textRect.width(), textRect.height());
+
+//	boxRect = QRect(x, y, boxRect.width(), boxRect.height());
+//	textRect = QRect(x + 3, y + 2, textRect.width(), textRect.height());
+
+	paint.setRenderHint(QPainter::Antialiasing, false);
+	paint.drawRect(boxRect);
+
+	paint.setRenderHint(QPainter::Antialiasing, true);
+	paint.drawText(textRect,
+		       Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap,
+		       label);
+
+///	if (p.label != "") {
+///	    paint.drawText(x + 5, y - paint.fontMetrics().height() + paint.fontMetrics().ascent(), p.label);
+///	}
+    }
+
+    paint.restore();
+
+    // looks like save/restore doesn't deal with this:
+    paint.setRenderHint(QPainter::Antialiasing, false);
+}
+
+void
+TextLayer::drawStart(QMouseEvent *e)
+{
+    std::cerr << "TextLayer::drawStart(" << e->x() << "," << e->y() << ")" << std::endl;
+
+    if (!m_model) {
+	std::cerr << "TextLayer::drawStart: no model" << std::endl;
+	return;
+    }
+
+    long frame = getFrameForX(e->x());
+    if (frame < 0) frame = 0;
+    frame = frame / m_model->getResolution() * m_model->getResolution();
+
+    float height = getHeightForY(e->y());
+
+    m_editingPoint = TextModel::Point(frame, height, "");
+    m_originalPoint = m_editingPoint;
+
+    if (m_editingCommand) m_editingCommand->finish();
+    m_editingCommand = new TextModel::EditCommand(m_model, "Add Label");
+    m_editingCommand->addPoint(m_editingPoint);
+
+    m_editing = true;
+}
+
+void
+TextLayer::drawDrag(QMouseEvent *e)
+{
+    std::cerr << "TextLayer::drawDrag(" << e->x() << "," << e->y() << ")" << std::endl;
+
+    if (!m_model || !m_editing) return;
+
+    long frame = getFrameForX(e->x());
+    if (frame < 0) frame = 0;
+    frame = frame / m_model->getResolution() * m_model->getResolution();
+
+    float height = getHeightForY(e->y());
+
+    m_editingCommand->deletePoint(m_editingPoint);
+    m_editingPoint.frame = frame;
+    m_editingPoint.height = height;
+    m_editingCommand->addPoint(m_editingPoint);
+}
+
+void
+TextLayer::drawEnd(QMouseEvent *e)
+{
+    std::cerr << "TextLayer::drawEnd(" << e->x() << "," << e->y() << ")" << std::endl;
+    if (!m_model || !m_editing) return;
+    m_editingCommand->finish();
+    m_editingCommand = 0;
+    m_editing = false;
+}
+
+void
+TextLayer::editStart(QMouseEvent *e)
+{
+    std::cerr << "TextLayer::editStart(" << e->x() << "," << e->y() << ")" << std::endl;
+
+    if (!m_model) return;
+
+    TextModel::PointList points = getLocalPoints(e->x(), e->y());
+    if (points.empty()) return;
+
+    m_editingPoint = *points.begin();
+    m_originalPoint = m_editingPoint;
+
+    if (m_editingCommand) {
+	m_editingCommand->finish();
+	m_editingCommand = 0;
+    }
+
+    m_editing = true;
+}
+
+void
+TextLayer::editDrag(QMouseEvent *e)
+{
+    if (!m_model || !m_editing) return;
+
+    long frame = getFrameForX(e->x());
+    if (frame < 0) frame = 0;
+    frame = frame / m_model->getResolution() * m_model->getResolution();
+
+    float height = getHeightForY(e->y());
+
+    if (!m_editingCommand) {
+	m_editingCommand = new TextModel::EditCommand(m_model, tr("Drag Label"));
+    }
+
+    m_editingCommand->deletePoint(m_editingPoint);
+    m_editingPoint.frame = frame;
+    m_editingPoint.height = height;
+    m_editingCommand->addPoint(m_editingPoint);
+}
+
+void
+TextLayer::editEnd(QMouseEvent *e)
+{
+    std::cerr << "TextLayer::editEnd(" << e->x() << "," << e->y() << ")" << std::endl;
+    if (!m_model || !m_editing) return;
+
+    if (m_editingCommand) {
+
+	QString newName = m_editingCommand->getName();
+
+	if (m_editingPoint.frame != m_originalPoint.frame) {
+	    if (m_editingPoint.height != m_originalPoint.height) {
+		newName = tr("Move Label");
+	    } else {
+		newName = tr("Relocate Label");
+	    }
+	} else {
+	    newName = tr("Change Point Height");
+	}
+
+	m_editingCommand->setName(newName);
+	m_editingCommand->finish();
+    }
+    
+    m_editingCommand = 0;
+    m_editing = false;
+}
+
+QString
+TextLayer::toXmlString(QString indent, QString extraAttributes) const
+{
+    return Layer::toXmlString(indent, extraAttributes +
+			      QString(" colour=\"%1\"")
+			      .arg(encodeColour(m_colour)));
+}
+
+void
+TextLayer::setProperties(const QXmlAttributes &attributes)
+{
+    QString colourSpec = attributes.value("colour");
+    if (colourSpec != "") {
+	QColor colour(colourSpec);
+	if (colour.isValid()) {
+	    setBaseColour(QColor(colourSpec));
+	}
+    }
+}
+
+
+#ifdef INCLUDE_MOCFILES
+#include "TextLayer.moc.cpp"
+#endif
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/layer/TextLayer.h	Mon Feb 20 13:33:36 2006 +0000
@@ -0,0 +1,85 @@
+/* -*- c-basic-offset: 4 -*-  vi:set ts=8 sts=4 sw=4: */
+
+/*
+    A waveform viewer and audio annotation editor.
+    Chris Cannam, Queen Mary University of London, 2005-2006
+    
+    This is experimental software.  Not for distribution.
+*/
+
+#ifndef _TEXT_LAYER_H_
+#define _TEXT_LAYER_H_
+
+#include "base/Layer.h"
+#include "model/TextModel.h"
+
+#include <QObject>
+#include <QColor>
+
+class View;
+class QPainter;
+
+class TextLayer : public Layer
+{
+    Q_OBJECT
+
+public:
+    TextLayer(View *w);
+
+    virtual void paint(QPainter &paint, QRect rect) const;
+
+    virtual QString getFeatureDescription(QPoint &) const;
+
+    virtual bool snapToFeatureFrame(int &frame,
+				    size_t &resolution,
+				    SnapType snap) const;
+
+    virtual void drawStart(QMouseEvent *);
+    virtual void drawDrag(QMouseEvent *);
+    virtual void drawEnd(QMouseEvent *);
+
+    virtual void editStart(QMouseEvent *);
+    virtual void editDrag(QMouseEvent *);
+    virtual void editEnd(QMouseEvent *);
+
+    virtual const Model *getModel() const { return m_model; }
+    void setModel(TextModel *model);
+
+    virtual PropertyList getProperties() const;
+    virtual PropertyType getPropertyType(const PropertyName &) const;
+    virtual int getPropertyRangeAndValue(const PropertyName &,
+					   int *min, int *max) const;
+    virtual QString getPropertyValueLabel(const PropertyName &,
+					  int value) const;
+    virtual void setProperty(const PropertyName &, int value);
+
+    void setBaseColour(QColor);
+    QColor getBaseColour() const { return m_colour; }
+
+    virtual bool isLayerScrollable() const;
+
+    virtual bool isLayerEditable() const { return true; }
+
+    virtual int getCompletion() const { return m_model->getCompletion(); }
+
+    virtual QString toXmlString(QString indent = "",
+				QString extraAttributes = "") const;
+
+    void setProperties(const QXmlAttributes &attributes);
+
+protected:
+    int getYForHeight(float height) const;
+    float getHeightForY(int y) const;
+
+    TextModel::PointList getLocalPoints(int x, int y) const;
+
+    TextModel *m_model;
+    bool m_editing;
+    TextModel::Point m_originalPoint;
+    TextModel::Point m_editingPoint;
+    TextModel::EditCommand *m_editingCommand;
+    QColor m_colour;
+};
+
+#endif
+
--- a/widgets/Pane.cpp	Fri Feb 17 18:11:08 2006 +0000
+++ b/widgets/Pane.cpp	Mon Feb 20 13:33:36 2006 +0000
@@ -492,7 +492,7 @@
 	    }
 	}
 
-	if (mode != ViewManager::DrawMode) {
+//!!!	if (mode != ViewManager::DrawMode) {
 
 	    bool previouslyIdentifying = m_identifyFeatures;
 	    m_identifyFeatures = true;
@@ -501,7 +501,7 @@
 		m_identifyPoint != prevPoint) {
 		update();
 	    }
-	}
+//	}
 
 	return;
     }
--- a/widgets/PropertyBox.cpp	Fri Feb 17 18:11:08 2006 +0000
+++ b/widgets/PropertyBox.cpp	Mon Feb 20 13:33:36 2006 +0000
@@ -148,7 +148,7 @@
 	gainDial->setFixedWidth(24);
 	gainDial->setFixedHeight(24);
 	gainDial->setNotchesVisible(false);
-	gainDial->setToolTip(tr("Layer playback level"));
+	gainDial->setToolTip(tr("Layer playback Level"));
 	gainDial->setDefaultValue(0);
 	connect(gainDial, SIGNAL(valueChanged(int)),
 		this, SLOT(playGainDialChanged(int)));
@@ -169,7 +169,7 @@
 	panDial->setFixedWidth(24);
 	panDial->setFixedHeight(24);
 	panDial->setNotchesVisible(false);
-	panDial->setToolTip(tr("Layer playback pan"));
+	panDial->setToolTip(tr("Layer playback Pan / Balance"));
 	panDial->setDefaultValue(0);
 	connect(panDial, SIGNAL(valueChanged(int)),
 		this, SLOT(playPanDialChanged(int)));