# HG changeset patch # User Chris Cannam # Date 1154347425 0 # Node ID cd5d7ff8ef38db407306e8ed02a77901db8d0289 * Reorganising code base. This revision will not compile. diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioCallbackPlaySource.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioCallbackPlaySource.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,1257 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "AudioCallbackPlaySource.h" + +#include "AudioGenerator.h" + +#include "base/Model.h" +#include "base/ViewManager.h" +#include "base/PlayParameterRepository.h" +#include "model/DenseTimeValueModel.h" +#include "model/SparseOneDimensionalModel.h" +#include "IntegerTimeStretcher.h" + +#include +#include + +//#define DEBUG_AUDIO_PLAY_SOURCE 1 +//#define DEBUG_AUDIO_PLAY_SOURCE_PLAYING 1 + +//const size_t AudioCallbackPlaySource::m_ringBufferSize = 102400; +const size_t AudioCallbackPlaySource::m_ringBufferSize = 131071; + +AudioCallbackPlaySource::AudioCallbackPlaySource(ViewManager *manager) : + m_viewManager(manager), + m_audioGenerator(new AudioGenerator()), + m_readBuffers(0), + m_writeBuffers(0), + m_readBufferFill(0), + m_writeBufferFill(0), + m_bufferScavenger(1), + m_sourceChannelCount(0), + m_blockSize(1024), + m_sourceSampleRate(0), + m_targetSampleRate(0), + m_playLatency(0), + m_playing(false), + m_exiting(false), + m_lastModelEndFrame(0), + m_outputLeft(0.0), + m_outputRight(0.0), + m_slowdownCounter(0), + m_timeStretcher(0), + m_fillThread(0), + m_converter(0) +{ + m_viewManager->setAudioPlaySource(this); + + connect(m_viewManager, SIGNAL(selectionChanged()), + this, SLOT(selectionChanged())); + connect(m_viewManager, SIGNAL(playLoopModeChanged()), + this, SLOT(playLoopModeChanged())); + connect(m_viewManager, SIGNAL(playSelectionModeChanged()), + this, SLOT(playSelectionModeChanged())); + + connect(PlayParameterRepository::getInstance(), + SIGNAL(playParametersChanged(PlayParameters *)), + this, SLOT(playParametersChanged(PlayParameters *))); +} + +AudioCallbackPlaySource::~AudioCallbackPlaySource() +{ + m_exiting = true; + + if (m_fillThread) { + m_condition.wakeAll(); + m_fillThread->wait(); + delete m_fillThread; + } + + clearModels(); + + if (m_readBuffers != m_writeBuffers) { + delete m_readBuffers; + } + + delete m_writeBuffers; + + delete m_audioGenerator; + + m_bufferScavenger.scavenge(true); +} + +void +AudioCallbackPlaySource::addModel(Model *model) +{ + if (m_models.find(model) != m_models.end()) return; + + bool canPlay = m_audioGenerator->addModel(model); + + m_mutex.lock(); + + m_models.insert(model); + if (model->getEndFrame() > m_lastModelEndFrame) { + m_lastModelEndFrame = model->getEndFrame(); + } + + bool buffersChanged = false, srChanged = false; + + size_t modelChannels = 1; + DenseTimeValueModel *dtvm = dynamic_cast(model); + if (dtvm) modelChannels = dtvm->getChannelCount(); + if (modelChannels > m_sourceChannelCount) { + m_sourceChannelCount = modelChannels; + } + +// std::cerr << "Adding model with " << modelChannels << " channels " << std::endl; + + if (m_sourceSampleRate == 0) { + + m_sourceSampleRate = model->getSampleRate(); + srChanged = true; + + } else if (model->getSampleRate() != m_sourceSampleRate) { + + // If this is a dense time-value model and we have no other, we + // can just switch to this model's sample rate + + if (dtvm) { + + bool conflicting = false; + + for (std::set::const_iterator i = m_models.begin(); + i != m_models.end(); ++i) { + if (*i != dtvm && dynamic_cast(*i)) { + std::cerr << "AudioCallbackPlaySource::addModel: Conflicting dense time-value model " << *i << " found" << std::endl; + conflicting = true; + break; + } + } + + if (conflicting) { + + std::cerr << "AudioCallbackPlaySource::addModel: ERROR: " + << "New model sample rate does not match" << std::endl + << "existing model(s) (new " << model->getSampleRate() + << " vs " << m_sourceSampleRate + << "), playback will be wrong" + << std::endl; + + emit sampleRateMismatch(model->getSampleRate(), m_sourceSampleRate, + false); + } else { + m_sourceSampleRate = model->getSampleRate(); + srChanged = true; + } + } + } + + if (!m_writeBuffers || (m_writeBuffers->size() < getTargetChannelCount())) { + clearRingBuffers(true, getTargetChannelCount()); + buffersChanged = true; + } else { + if (canPlay) clearRingBuffers(true); + } + + if (buffersChanged || srChanged) { + if (m_converter) { + src_delete(m_converter); + m_converter = 0; + } + } + + m_mutex.unlock(); + + m_audioGenerator->setTargetChannelCount(getTargetChannelCount()); + + if (!m_fillThread) { + m_fillThread = new AudioCallbackPlaySourceFillThread(*this); + m_fillThread->start(); + } + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "AudioCallbackPlaySource::addModel: emitting modelReplaced" << std::endl; +#endif + + if (buffersChanged || srChanged) { + emit modelReplaced(); + } + + m_condition.wakeAll(); +} + +void +AudioCallbackPlaySource::removeModel(Model *model) +{ + m_mutex.lock(); + + m_models.erase(model); + + if (m_models.empty()) { + if (m_converter) { + src_delete(m_converter); + m_converter = 0; + } + m_sourceSampleRate = 0; + } + + size_t lastEnd = 0; + for (std::set::const_iterator i = m_models.begin(); + i != m_models.end(); ++i) { +// std::cerr << "AudioCallbackPlaySource::removeModel(" << model << "): checking end frame on model " << *i << std::endl; + if ((*i)->getEndFrame() > lastEnd) lastEnd = (*i)->getEndFrame(); +// std::cerr << "(done, lastEnd now " << lastEnd << ")" << std::endl; + } + m_lastModelEndFrame = lastEnd; + + m_mutex.unlock(); + + m_audioGenerator->removeModel(model); + + clearRingBuffers(); +} + +void +AudioCallbackPlaySource::clearModels() +{ + m_mutex.lock(); + + m_models.clear(); + + if (m_converter) { + src_delete(m_converter); + m_converter = 0; + } + + m_lastModelEndFrame = 0; + + m_sourceSampleRate = 0; + + m_mutex.unlock(); + + m_audioGenerator->clearModels(); +} + +void +AudioCallbackPlaySource::clearRingBuffers(bool haveLock, size_t count) +{ + if (!haveLock) m_mutex.lock(); + + if (count == 0) { + if (m_writeBuffers) count = m_writeBuffers->size(); + } + + size_t sf = m_readBufferFill; + RingBuffer *rb = getReadRingBuffer(0); + if (rb) { + //!!! This is incorrect if we're in a non-contiguous selection + //Same goes for all related code (subtracting the read space + //from the fill frame to try to establish where the effective + //pre-resample/timestretch read pointer is) + size_t rs = rb->getReadSpace(); + if (rs < sf) sf -= rs; + else sf = 0; + } + m_writeBufferFill = sf; + + if (m_readBuffers != m_writeBuffers) { + delete m_writeBuffers; + } + + m_writeBuffers = new RingBufferVector; + + for (size_t i = 0; i < count; ++i) { + m_writeBuffers->push_back(new RingBuffer(m_ringBufferSize)); + } + +// std::cerr << "AudioCallbackPlaySource::clearRingBuffers: Created " +// << count << " write buffers" << std::endl; + + if (!haveLock) { + m_mutex.unlock(); + } +} + +void +AudioCallbackPlaySource::play(size_t startFrame) +{ + if (m_viewManager->getPlaySelectionMode() && + !m_viewManager->getSelections().empty()) { + MultiSelection::SelectionList selections = m_viewManager->getSelections(); + MultiSelection::SelectionList::iterator i = selections.begin(); + if (i != selections.end()) { + if (startFrame < i->getStartFrame()) { + startFrame = i->getStartFrame(); + } else { + MultiSelection::SelectionList::iterator j = selections.end(); + --j; + if (startFrame >= j->getEndFrame()) { + startFrame = i->getStartFrame(); + } + } + } + } else { + if (startFrame >= m_lastModelEndFrame) { + startFrame = 0; + } + } + + // The fill thread will automatically empty its buffers before + // starting again if we have not so far been playing, but not if + // we're just re-seeking. + + m_mutex.lock(); + if (m_playing) { + m_readBufferFill = m_writeBufferFill = startFrame; + if (m_readBuffers) { + for (size_t c = 0; c < getTargetChannelCount(); ++c) { + RingBuffer *rb = getReadRingBuffer(c); + if (rb) rb->reset(); + } + } + if (m_converter) src_reset(m_converter); + } else { + if (m_converter) src_reset(m_converter); + m_readBufferFill = m_writeBufferFill = startFrame; + } + m_mutex.unlock(); + + m_audioGenerator->reset(); + + bool changed = !m_playing; + m_playing = true; + m_condition.wakeAll(); + if (changed) emit playStatusChanged(m_playing); +} + +void +AudioCallbackPlaySource::stop() +{ + bool changed = m_playing; + m_playing = false; + m_condition.wakeAll(); + if (changed) emit playStatusChanged(m_playing); +} + +void +AudioCallbackPlaySource::selectionChanged() +{ + if (m_viewManager->getPlaySelectionMode()) { + clearRingBuffers(); + } +} + +void +AudioCallbackPlaySource::playLoopModeChanged() +{ + clearRingBuffers(); +} + +void +AudioCallbackPlaySource::playSelectionModeChanged() +{ + if (!m_viewManager->getSelections().empty()) { + clearRingBuffers(); + } +} + +void +AudioCallbackPlaySource::playParametersChanged(PlayParameters *params) +{ + clearRingBuffers(); +} + +void +AudioCallbackPlaySource::setTargetBlockSize(size_t size) +{ +// std::cerr << "AudioCallbackPlaySource::setTargetBlockSize() -> " << size << std::endl; + assert(size < m_ringBufferSize); + m_blockSize = size; +} + +size_t +AudioCallbackPlaySource::getTargetBlockSize() const +{ +// std::cerr << "AudioCallbackPlaySource::getTargetBlockSize() -> " << m_blockSize << std::endl; + return m_blockSize; +} + +void +AudioCallbackPlaySource::setTargetPlayLatency(size_t latency) +{ + m_playLatency = latency; +} + +size_t +AudioCallbackPlaySource::getTargetPlayLatency() const +{ + return m_playLatency; +} + +size_t +AudioCallbackPlaySource::getCurrentPlayingFrame() +{ + bool resample = false; + double ratio = 1.0; + + if (getSourceSampleRate() != getTargetSampleRate()) { + resample = true; + ratio = double(getSourceSampleRate()) / double(getTargetSampleRate()); + } + + size_t readSpace = 0; + for (size_t c = 0; c < getTargetChannelCount(); ++c) { + RingBuffer *rb = getReadRingBuffer(c); + if (rb) { + size_t spaceHere = rb->getReadSpace(); + if (c == 0 || spaceHere < readSpace) readSpace = spaceHere; + } + } + + if (resample) { + readSpace = size_t(readSpace * ratio + 0.1); + } + + size_t latency = m_playLatency; + if (resample) latency = size_t(m_playLatency * ratio + 0.1); + + TimeStretcherData *timeStretcher = m_timeStretcher; + if (timeStretcher) { + latency += timeStretcher->getStretcher(0)->getProcessingLatency(); + } + + latency += readSpace; + size_t bufferedFrame = m_readBufferFill; + + bool looping = m_viewManager->getPlayLoopMode(); + bool constrained = (m_viewManager->getPlaySelectionMode() && + !m_viewManager->getSelections().empty()); + + size_t framePlaying = bufferedFrame; + + if (looping && !constrained) { + while (framePlaying < latency) framePlaying += m_lastModelEndFrame; + } + + if (framePlaying > latency) framePlaying -= latency; + else framePlaying = 0; + + if (!constrained) { + if (!looping && framePlaying > m_lastModelEndFrame) { + framePlaying = m_lastModelEndFrame; + stop(); + } + return framePlaying; + } + + MultiSelection::SelectionList selections = m_viewManager->getSelections(); + MultiSelection::SelectionList::const_iterator i; + + i = selections.begin(); + size_t rangeStart = i->getStartFrame(); + + i = selections.end(); + --i; + size_t rangeEnd = i->getEndFrame(); + + for (i = selections.begin(); i != selections.end(); ++i) { + if (i->contains(bufferedFrame)) break; + } + + size_t f = bufferedFrame; + +// std::cerr << "getCurrentPlayingFrame: f=" << f << ", latency=" << latency << ", rangeEnd=" << rangeEnd << std::endl; + + if (i == selections.end()) { + --i; + if (i->getEndFrame() + latency < f) { +// std::cerr << "framePlaying = " << framePlaying << ", rangeEnd = " << rangeEnd << std::endl; + + if (!looping && (framePlaying > rangeEnd)) { +// std::cerr << "STOPPING" << std::endl; + stop(); + return rangeEnd; + } else { + return framePlaying; + } + } else { +// std::cerr << "latency <- " << latency << "-(" << f << "-" << i->getEndFrame() << ")" << std::endl; + latency -= (f - i->getEndFrame()); + f = i->getEndFrame(); + } + } + +// std::cerr << "i=(" << i->getStartFrame() << "," << i->getEndFrame() << ") f=" << f << ", latency=" << latency << std::endl; + + while (latency > 0) { + size_t offset = f - i->getStartFrame(); + if (offset >= latency) { + if (f > latency) { + framePlaying = f - latency; + } else { + framePlaying = 0; + } + break; + } else { + if (i == selections.begin()) { + if (looping) { + i = selections.end(); + } + } + latency -= offset; + --i; + f = i->getEndFrame(); + } + } + + return framePlaying; +} + +void +AudioCallbackPlaySource::setOutputLevels(float left, float right) +{ + m_outputLeft = left; + m_outputRight = right; +} + +bool +AudioCallbackPlaySource::getOutputLevels(float &left, float &right) +{ + left = m_outputLeft; + right = m_outputRight; + return true; +} + +void +AudioCallbackPlaySource::setTargetSampleRate(size_t sr) +{ + m_targetSampleRate = sr; + + if (getSourceSampleRate() != getTargetSampleRate()) { + + int err = 0; + m_converter = src_new(SRC_SINC_BEST_QUALITY, + getTargetChannelCount(), &err); + if (!m_converter) { + std::cerr + << "AudioCallbackPlaySource::setModel: ERROR in creating samplerate converter: " + << src_strerror(err) << std::endl; + + emit sampleRateMismatch(getSourceSampleRate(), + getTargetSampleRate(), + false); + } else { + + emit sampleRateMismatch(getSourceSampleRate(), + getTargetSampleRate(), + true); + } + } +} + +size_t +AudioCallbackPlaySource::getTargetSampleRate() const +{ + if (m_targetSampleRate) return m_targetSampleRate; + else return getSourceSampleRate(); +} + +size_t +AudioCallbackPlaySource::getSourceChannelCount() const +{ + return m_sourceChannelCount; +} + +size_t +AudioCallbackPlaySource::getTargetChannelCount() const +{ + if (m_sourceChannelCount < 2) return 2; + return m_sourceChannelCount; +} + +size_t +AudioCallbackPlaySource::getSourceSampleRate() const +{ + return m_sourceSampleRate; +} + +AudioCallbackPlaySource::TimeStretcherData::TimeStretcherData(size_t channels, + size_t factor, + size_t blockSize) : + m_factor(factor), + m_blockSize(blockSize) +{ +// std::cerr << "TimeStretcherData::TimeStretcherData(" << channels << ", " << factor << ", " << blockSize << ")" << std::endl; + + for (size_t ch = 0; ch < channels; ++ch) { + m_stretcher[ch] = StretcherBuffer + //!!! We really need to measure performance and work out + //what sort of quality level to use -- or at least to + //allow the user to configure it + (new IntegerTimeStretcher(factor, blockSize, 128), + new float[blockSize * factor]); + } + m_stretchInputBuffer = new float[blockSize]; +} + +AudioCallbackPlaySource::TimeStretcherData::~TimeStretcherData() +{ +// std::cerr << "TimeStretcherData::~TimeStretcherData" << std::endl; + + while (!m_stretcher.empty()) { + delete m_stretcher.begin()->second.first; + delete[] m_stretcher.begin()->second.second; + m_stretcher.erase(m_stretcher.begin()); + } + delete m_stretchInputBuffer; +} + +IntegerTimeStretcher * +AudioCallbackPlaySource::TimeStretcherData::getStretcher(size_t channel) +{ + return m_stretcher[channel].first; +} + +float * +AudioCallbackPlaySource::TimeStretcherData::getOutputBuffer(size_t channel) +{ + return m_stretcher[channel].second; +} + +float * +AudioCallbackPlaySource::TimeStretcherData::getInputBuffer() +{ + return m_stretchInputBuffer; +} + +void +AudioCallbackPlaySource::TimeStretcherData::run(size_t channel) +{ + getStretcher(channel)->process(getInputBuffer(), + getOutputBuffer(channel), + m_blockSize); +} + +void +AudioCallbackPlaySource::setSlowdownFactor(size_t factor) +{ + // Avoid locks -- create, assign, mark old one for scavenging + // later (as a call to getSourceSamples may still be using it) + + TimeStretcherData *existingStretcher = m_timeStretcher; + + if (existingStretcher && existingStretcher->getFactor() == factor) { + return; + } + + if (factor > 1) { + TimeStretcherData *newStretcher = new TimeStretcherData + (getTargetChannelCount(), factor, getTargetBlockSize()); + m_slowdownCounter = 0; + m_timeStretcher = newStretcher; + } else { + m_timeStretcher = 0; + } + + if (existingStretcher) { + m_timeStretcherScavenger.claim(existingStretcher); + } +} + +size_t +AudioCallbackPlaySource::getSourceSamples(size_t count, float **buffer) +{ + if (!m_playing) { + for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) { + for (size_t i = 0; i < count; ++i) { + buffer[ch][i] = 0.0; + } + } + return 0; + } + + TimeStretcherData *timeStretcher = m_timeStretcher; + + if (!timeStretcher || timeStretcher->getFactor() == 1) { + + size_t got = 0; + + for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) { + + RingBuffer *rb = getReadRingBuffer(ch); + + if (rb) { + + // this is marginally more likely to leave our channels in + // sync after a processing failure than just passing "count": + size_t request = count; + if (ch > 0) request = got; + + got = rb->read(buffer[ch], request); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE_PLAYING + std::cout << "AudioCallbackPlaySource::getSamples: got " << got << " samples on channel " << ch << ", signalling for more (possibly)" << std::endl; +#endif + } + + for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) { + for (size_t i = got; i < count; ++i) { + buffer[ch][i] = 0.0; + } + } + } + + m_condition.wakeAll(); + return got; + } + + if (m_slowdownCounter == 0) { + + size_t got = 0; + float *ib = timeStretcher->getInputBuffer(); + + for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) { + + RingBuffer *rb = getReadRingBuffer(ch); + + if (rb) { + + size_t request = count; + if (ch > 0) request = got; // see above + got = rb->read(buffer[ch], request); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "AudioCallbackPlaySource::getSamples: got " << got << " samples on channel " << ch << ", running time stretcher" << std::endl; +#endif + + for (size_t i = 0; i < count; ++i) { + ib[i] = buffer[ch][i]; + } + + timeStretcher->run(ch); + } + } + + } else if (m_slowdownCounter >= timeStretcher->getFactor()) { + // reset this in case the factor has changed leaving the + // counter out of range + m_slowdownCounter = 0; + } + + for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) { + + float *ob = timeStretcher->getOutputBuffer(ch); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "AudioCallbackPlaySource::getSamples: Copying from (" << (m_slowdownCounter * count) << "," << count << ") to buffer" << std::endl; +#endif + + for (size_t i = 0; i < count; ++i) { + buffer[ch][i] = ob[m_slowdownCounter * count + i]; + } + } + +//!!! if (m_slowdownCounter == 0) m_condition.wakeAll(); + m_slowdownCounter = (m_slowdownCounter + 1) % timeStretcher->getFactor(); + return count; +} + +// Called from fill thread, m_playing true, mutex held +bool +AudioCallbackPlaySource::fillBuffers() +{ + static float *tmp = 0; + static size_t tmpSize = 0; + + size_t space = 0; + for (size_t c = 0; c < getTargetChannelCount(); ++c) { + RingBuffer *wb = getWriteRingBuffer(c); + if (wb) { + size_t spaceHere = wb->getWriteSpace(); + if (c == 0 || spaceHere < space) space = spaceHere; + } + } + + if (space == 0) return false; + + size_t f = m_writeBufferFill; + + bool readWriteEqual = (m_readBuffers == m_writeBuffers); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "AudioCallbackPlaySourceFillThread: filling " << space << " frames" << std::endl; +#endif + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "buffered to " << f << " already" << std::endl; +#endif + + bool resample = (getSourceSampleRate() != getTargetSampleRate()); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << (resample ? "" : "not ") << "resampling (source " << getSourceSampleRate() << ", target " << getTargetSampleRate() << ")" << std::endl; +#endif + + size_t channels = getTargetChannelCount(); + + size_t orig = space; + size_t got = 0; + + static float **bufferPtrs = 0; + static size_t bufferPtrCount = 0; + + if (bufferPtrCount < channels) { + if (bufferPtrs) delete[] bufferPtrs; + bufferPtrs = new float *[channels]; + bufferPtrCount = channels; + } + + size_t generatorBlockSize = m_audioGenerator->getBlockSize(); + + if (resample && !m_converter) { + static bool warned = false; + if (!warned) { + std::cerr << "WARNING: sample rates differ, but no converter available!" << std::endl; + warned = true; + } + } + + if (resample && m_converter) { + + double ratio = + double(getTargetSampleRate()) / double(getSourceSampleRate()); + orig = size_t(orig / ratio + 0.1); + + // orig must be a multiple of generatorBlockSize + orig = (orig / generatorBlockSize) * generatorBlockSize; + if (orig == 0) return false; + + size_t work = std::max(orig, space); + + // We only allocate one buffer, but we use it in two halves. + // We place the non-interleaved values in the second half of + // the buffer (orig samples for channel 0, orig samples for + // channel 1 etc), and then interleave them into the first + // half of the buffer. Then we resample back into the second + // half (interleaved) and de-interleave the results back to + // the start of the buffer for insertion into the ringbuffers. + // What a faff -- especially as we've already de-interleaved + // the audio data from the source file elsewhere before we + // even reach this point. + + if (tmpSize < channels * work * 2) { + delete[] tmp; + tmp = new float[channels * work * 2]; + tmpSize = channels * work * 2; + } + + float *nonintlv = tmp + channels * work; + float *intlv = tmp; + float *srcout = tmp + channels * work; + + for (size_t c = 0; c < channels; ++c) { + for (size_t i = 0; i < orig; ++i) { + nonintlv[channels * i + c] = 0.0f; + } + } + + for (size_t c = 0; c < channels; ++c) { + bufferPtrs[c] = nonintlv + c * orig; + } + + got = mixModels(f, orig, bufferPtrs); + + // and interleave into first half + for (size_t c = 0; c < channels; ++c) { + for (size_t i = 0; i < got; ++i) { + float sample = nonintlv[c * got + i]; + intlv[channels * i + c] = sample; + } + } + + SRC_DATA data; + data.data_in = intlv; + data.data_out = srcout; + data.input_frames = got; + data.output_frames = work; + data.src_ratio = ratio; + data.end_of_input = 0; + + int err = src_process(m_converter, &data); +// size_t toCopy = size_t(work * ratio + 0.1); + size_t toCopy = size_t(got * ratio + 0.1); + + if (err) { + std::cerr + << "AudioCallbackPlaySourceFillThread: ERROR in samplerate conversion: " + << src_strerror(err) << std::endl; + //!!! Then what? + } else { + got = data.input_frames_used; + toCopy = data.output_frames_gen; +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "Resampled " << got << " frames to " << toCopy << " frames" << std::endl; +#endif + } + + for (size_t c = 0; c < channels; ++c) { + for (size_t i = 0; i < toCopy; ++i) { + tmp[i] = srcout[channels * i + c]; + } + RingBuffer *wb = getWriteRingBuffer(c); + if (wb) wb->write(tmp, toCopy); + } + + m_writeBufferFill = f; + if (readWriteEqual) m_readBufferFill = f; + + } else { + + // space must be a multiple of generatorBlockSize + space = (space / generatorBlockSize) * generatorBlockSize; + if (space == 0) return false; + + if (tmpSize < channels * space) { + delete[] tmp; + tmp = new float[channels * space]; + tmpSize = channels * space; + } + + for (size_t c = 0; c < channels; ++c) { + + bufferPtrs[c] = tmp + c * space; + + for (size_t i = 0; i < space; ++i) { + tmp[c * space + i] = 0.0f; + } + } + + size_t got = mixModels(f, space, bufferPtrs); + + for (size_t c = 0; c < channels; ++c) { + + RingBuffer *wb = getWriteRingBuffer(c); + if (wb) wb->write(bufferPtrs[c], got); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + if (wb) + std::cerr << "Wrote " << got << " frames for ch " << c << ", now " + << wb->getReadSpace() << " to read" + << std::endl; +#endif + } + + m_writeBufferFill = f; + if (readWriteEqual) m_readBufferFill = f; + + //!!! how do we know when ended? need to mark up a fully-buffered flag and check this if we find the buffers empty in getSourceSamples + } + + return true; +} + +size_t +AudioCallbackPlaySource::mixModels(size_t &frame, size_t count, float **buffers) +{ + size_t processed = 0; + size_t chunkStart = frame; + size_t chunkSize = count; + size_t selectionSize = 0; + size_t nextChunkStart = chunkStart + chunkSize; + + bool looping = m_viewManager->getPlayLoopMode(); + bool constrained = (m_viewManager->getPlaySelectionMode() && + !m_viewManager->getSelections().empty()); + + static float **chunkBufferPtrs = 0; + static size_t chunkBufferPtrCount = 0; + size_t channels = getTargetChannelCount(); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "Selection playback: start " << frame << ", size " << count <<", channels " << channels << std::endl; +#endif + + if (chunkBufferPtrCount < channels) { + if (chunkBufferPtrs) delete[] chunkBufferPtrs; + chunkBufferPtrs = new float *[channels]; + chunkBufferPtrCount = channels; + } + + for (size_t c = 0; c < channels; ++c) { + chunkBufferPtrs[c] = buffers[c]; + } + + while (processed < count) { + + chunkSize = count - processed; + nextChunkStart = chunkStart + chunkSize; + selectionSize = 0; + + size_t fadeIn = 0, fadeOut = 0; + + if (constrained) { + + Selection selection = + m_viewManager->getContainingSelection(chunkStart, true); + + if (selection.isEmpty()) { + if (looping) { + selection = *m_viewManager->getSelections().begin(); + chunkStart = selection.getStartFrame(); + fadeIn = 50; + } + } + + if (selection.isEmpty()) { + + chunkSize = 0; + nextChunkStart = chunkStart; + + } else { + + selectionSize = + selection.getEndFrame() - + selection.getStartFrame(); + + if (chunkStart < selection.getStartFrame()) { + chunkStart = selection.getStartFrame(); + fadeIn = 50; + } + + nextChunkStart = chunkStart + chunkSize; + + if (nextChunkStart >= selection.getEndFrame()) { + nextChunkStart = selection.getEndFrame(); + fadeOut = 50; + } + + chunkSize = nextChunkStart - chunkStart; + } + + } else if (looping && m_lastModelEndFrame > 0) { + + if (chunkStart >= m_lastModelEndFrame) { + chunkStart = 0; + } + if (chunkSize > m_lastModelEndFrame - chunkStart) { + chunkSize = m_lastModelEndFrame - chunkStart; + } + nextChunkStart = chunkStart + chunkSize; + } + +// std::cerr << "chunkStart " << chunkStart << ", chunkSize " << chunkSize << ", nextChunkStart " << nextChunkStart << ", frame " << frame << ", count " << count << ", processed " << processed << std::endl; + + if (!chunkSize) { +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "Ending selection playback at " << nextChunkStart << std::endl; +#endif + // We need to maintain full buffers so that the other + // thread can tell where it's got to in the playback -- so + // return the full amount here + frame = frame + count; + return count; + } + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "Selection playback: chunk at " << chunkStart << " -> " << nextChunkStart << " (size " << chunkSize << ")" << std::endl; +#endif + + size_t got = 0; + + if (selectionSize < 100) { + fadeIn = 0; + fadeOut = 0; + } else if (selectionSize < 300) { + if (fadeIn > 0) fadeIn = 10; + if (fadeOut > 0) fadeOut = 10; + } + + if (fadeIn > 0) { + if (processed * 2 < fadeIn) { + fadeIn = processed * 2; + } + } + + if (fadeOut > 0) { + if ((count - processed - chunkSize) * 2 < fadeOut) { + fadeOut = (count - processed - chunkSize) * 2; + } + } + + for (std::set::iterator mi = m_models.begin(); + mi != m_models.end(); ++mi) { + + got = m_audioGenerator->mixModel(*mi, chunkStart, + chunkSize, chunkBufferPtrs, + fadeIn, fadeOut); + } + + for (size_t c = 0; c < channels; ++c) { + chunkBufferPtrs[c] += chunkSize; + } + + processed += chunkSize; + chunkStart = nextChunkStart; + } + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "Returning selection playback " << processed << " frames to " << nextChunkStart << std::endl; +#endif + + frame = nextChunkStart; + return processed; +} + +void +AudioCallbackPlaySource::unifyRingBuffers() +{ + if (m_readBuffers == m_writeBuffers) return; + + // only unify if there will be something to read + for (size_t c = 0; c < getTargetChannelCount(); ++c) { + RingBuffer *wb = getWriteRingBuffer(c); + if (wb) { + if (wb->getReadSpace() < m_blockSize * 2) { + if ((m_writeBufferFill + m_blockSize * 2) < + m_lastModelEndFrame) { + // OK, we don't have enough and there's more to + // read -- don't unify until we can do better + return; + } + } + break; + } + } + + size_t rf = m_readBufferFill; + RingBuffer *rb = getReadRingBuffer(0); + if (rb) { + size_t rs = rb->getReadSpace(); + //!!! incorrect when in non-contiguous selection, see comments elsewhere +// std::cerr << "rs = " << rs << std::endl; + if (rs < rf) rf -= rs; + else rf = 0; + } + + //std::cerr << "m_readBufferFill = " << m_readBufferFill << ", rf = " << rf << ", m_writeBufferFill = " << m_writeBufferFill << std::endl; + + size_t wf = m_writeBufferFill; + size_t skip = 0; + for (size_t c = 0; c < getTargetChannelCount(); ++c) { + RingBuffer *wb = getWriteRingBuffer(c); + if (wb) { + if (c == 0) { + + size_t wrs = wb->getReadSpace(); +// std::cerr << "wrs = " << wrs << std::endl; + + if (wrs < wf) wf -= wrs; + else wf = 0; +// std::cerr << "wf = " << wf << std::endl; + + if (wf < rf) skip = rf - wf; + if (skip == 0) break; + } + +// std::cerr << "skipping " << skip << std::endl; + wb->skip(skip); + } + } + + m_bufferScavenger.claim(m_readBuffers); + m_readBuffers = m_writeBuffers; + m_readBufferFill = m_writeBufferFill; +// std::cerr << "unified" << std::endl; +} + +void +AudioCallbackPlaySource::AudioCallbackPlaySourceFillThread::run() +{ + AudioCallbackPlaySource &s(m_source); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "AudioCallbackPlaySourceFillThread starting" << std::endl; +#endif + + s.m_mutex.lock(); + + bool previouslyPlaying = s.m_playing; + bool work = false; + + while (!s.m_exiting) { + + s.unifyRingBuffers(); + s.m_bufferScavenger.scavenge(); + s.m_timeStretcherScavenger.scavenge(); + + if (work && s.m_playing && s.getSourceSampleRate()) { + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "AudioCallbackPlaySourceFillThread: not waiting" << std::endl; +#endif + + s.m_mutex.unlock(); + s.m_mutex.lock(); + + } else { + + float ms = 100; + if (s.getSourceSampleRate() > 0) { + ms = float(m_ringBufferSize) / float(s.getSourceSampleRate()) * 1000.0; + } + + if (s.m_playing) ms /= 10; + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "AudioCallbackPlaySourceFillThread: waiting for " << ms << "ms..." << std::endl; +#endif + + s.m_condition.wait(&s.m_mutex, size_t(ms)); + } + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "AudioCallbackPlaySourceFillThread: awoken" << std::endl; +#endif + + work = false; + + if (!s.getSourceSampleRate()) continue; + + bool playing = s.m_playing; + + if (playing && !previouslyPlaying) { +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "AudioCallbackPlaySourceFillThread: playback state changed, resetting" << std::endl; +#endif + for (size_t c = 0; c < s.getTargetChannelCount(); ++c) { + RingBuffer *rb = s.getReadRingBuffer(c); + if (rb) rb->reset(); + } + } + previouslyPlaying = playing; + + work = s.fillBuffers(); + } + + s.m_mutex.unlock(); +} + + + +#ifdef INCLUDE_MOCFILES +#include "AudioCallbackPlaySource.moc.cpp" +#endif + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioCallbackPlaySource.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioCallbackPlaySource.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,306 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _AUDIO_CALLBACK_PLAY_SOURCE_H_ +#define _AUDIO_CALLBACK_PLAY_SOURCE_H_ + +#include "base/RingBuffer.h" +#include "base/AudioPlaySource.h" +#include "base/Scavenger.h" + +#include +#include +#include + +#include "base/Thread.h" + +#include + +#include +#include + +class Model; +class ViewManager; +class AudioGenerator; +class PlayParameters; +class IntegerTimeStretcher; + +/** + * AudioCallbackPlaySource manages audio data supply to callback-based + * audio APIs such as JACK or CoreAudio. It maintains one ring buffer + * per channel, filled during playback by a non-realtime thread, and + * provides a method for a realtime thread to pick up the latest + * available sample data from these buffers. + */ +class AudioCallbackPlaySource : public virtual QObject, + public AudioPlaySource +{ + Q_OBJECT + +public: + AudioCallbackPlaySource(ViewManager *); + virtual ~AudioCallbackPlaySource(); + + /** + * Add a data model to be played from. The source can mix + * playback from a number of sources including dense and sparse + * models. The models must match in sample rate, but they don't + * have to have identical numbers of channels. + */ + virtual void addModel(Model *model); + + /** + * Remove a model. + */ + virtual void removeModel(Model *model); + + /** + * Remove all models. (Silence will ensue.) + */ + virtual void clearModels(); + + /** + * Start making data available in the ring buffers for playback, + * from the given frame. If playback is already under way, reseek + * to the given frame and continue. + */ + virtual void play(size_t startFrame); + + /** + * Stop playback and ensure that no more data is returned. + */ + virtual void stop(); + + /** + * Return whether playback is currently supposed to be happening. + */ + virtual bool isPlaying() const { return m_playing; } + + /** + * Return the frame number that is currently expected to be coming + * out of the speakers. (i.e. compensating for playback latency.) + */ + virtual size_t getCurrentPlayingFrame(); + + /** + * Set the block size of the target audio device. This should + * be called by the target class. + */ + void setTargetBlockSize(size_t); + + /** + * Get the block size of the target audio device. + */ + size_t getTargetBlockSize() const; + + /** + * Set the playback latency of the target audio device, in frames + * at the target sample rate. This is the difference between the + * frame currently "leaving the speakers" and the last frame (or + * highest last frame across all channels) requested via + * getSamples(). The default is zero. + */ + void setTargetPlayLatency(size_t); + + /** + * Get the playback latency of the target audio device. + */ + size_t getTargetPlayLatency() const; + + /** + * Specify that the target audio device has a fixed sample rate + * (i.e. cannot accommodate arbitrary sample rates based on the + * source). If the target sets this to something other than the + * source sample rate, this class will resample automatically to + * fit. + */ + void setTargetSampleRate(size_t); + + /** + * Return the sample rate set by the target audio device (or the + * source sample rate if the target hasn't set one). + */ + virtual size_t getTargetSampleRate() const; + + /** + * Set the current output levels for metering (for call from the + * target) + */ + void setOutputLevels(float left, float right); + + /** + * Return the current (or thereabouts) output levels in the range + * 0.0 -> 1.0, for metering purposes. + */ + virtual bool getOutputLevels(float &left, float &right); + + /** + * Get the number of channels of audio that in the source models. + * This may safely be called from a realtime thread. Returns 0 if + * there is no source yet available. + */ + size_t getSourceChannelCount() const; + + /** + * Get the number of channels of audio that will be provided + * to the play target. This may be more than the source channel + * count: for example, a mono source will provide 2 channels + * after pan. + * This may safely be called from a realtime thread. Returns 0 if + * there is no source yet available. + */ + size_t getTargetChannelCount() const; + + /** + * Get the actual sample rate of the source material. This may + * safely be called from a realtime thread. Returns 0 if there is + * no source yet available. + */ + size_t getSourceSampleRate() const; + + /** + * Get "count" samples (at the target sample rate) of the mixed + * audio data, in all channels. This may safely be called from a + * realtime thread. + */ + size_t getSourceSamples(size_t count, float **buffer); + + void setSlowdownFactor(size_t factor); + +signals: + void modelReplaced(); + + void playStatusChanged(bool isPlaying); + + void sampleRateMismatch(size_t requested, size_t available, bool willResample); + +protected slots: + void selectionChanged(); + void playLoopModeChanged(); + void playSelectionModeChanged(); + void playParametersChanged(PlayParameters *); + +protected: + ViewManager *m_viewManager; + AudioGenerator *m_audioGenerator; + + class RingBufferVector : public std::vector *> { + public: + virtual ~RingBufferVector() { + while (!empty()) { + delete *begin(); + erase(begin()); + } + } + }; + + std::set m_models; + RingBufferVector *m_readBuffers; + RingBufferVector *m_writeBuffers; + size_t m_readBufferFill; + size_t m_writeBufferFill; + Scavenger m_bufferScavenger; + size_t m_sourceChannelCount; + size_t m_blockSize; + size_t m_sourceSampleRate; + size_t m_targetSampleRate; + size_t m_playLatency; + bool m_playing; + bool m_exiting; + size_t m_lastModelEndFrame; + static const size_t m_ringBufferSize; + float m_outputLeft; + float m_outputRight; + + RingBuffer *getWriteRingBuffer(size_t c) { + if (m_writeBuffers && c < m_writeBuffers->size()) { + return (*m_writeBuffers)[c]; + } else { + return 0; + } + } + + RingBuffer *getReadRingBuffer(size_t c) { + RingBufferVector *rb = m_readBuffers; + if (rb && c < rb->size()) { + return (*rb)[c]; + } else { + return 0; + } + } + + void clearRingBuffers(bool haveLock = false, size_t count = 0); + void unifyRingBuffers(); + + class TimeStretcherData + { + public: + TimeStretcherData(size_t channels, size_t factor, size_t blockSize); + ~TimeStretcherData(); + + size_t getFactor() const { return m_factor; } + IntegerTimeStretcher *getStretcher(size_t channel); + float *getOutputBuffer(size_t channel); + float *getInputBuffer(); + + void run(size_t channel); + + protected: + TimeStretcherData(const TimeStretcherData &); // not provided + TimeStretcherData &operator=(const TimeStretcherData &); // not provided + + typedef std::pair StretcherBuffer; + std::map m_stretcher; + float *m_stretchInputBuffer; + size_t m_factor; + size_t m_blockSize; + }; + + size_t m_slowdownCounter; + TimeStretcherData *m_timeStretcher; + Scavenger m_timeStretcherScavenger; + + // Called from fill thread, m_playing true, mutex held + // Return true if work done + bool fillBuffers(); + + // Called from fillBuffers. Return the number of frames written, + // which will be count or fewer. Return in the frame argument the + // new buffered frame position (which may be earlier than the + // frame argument passed in, in the case of looping). + size_t mixModels(size_t &frame, size_t count, float **buffers); + + class AudioCallbackPlaySourceFillThread : public Thread + { + public: + AudioCallbackPlaySourceFillThread(AudioCallbackPlaySource &source) : + Thread(Thread::NonRTThread), + m_source(source) { } + + virtual void run(); + + protected: + AudioCallbackPlaySource &m_source; + }; + + QMutex m_mutex; + QWaitCondition m_condition; + AudioCallbackPlaySourceFillThread *m_fillThread; + SRC_STATE *m_converter; +}; + +#endif + + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioCallbackPlayTarget.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioCallbackPlayTarget.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,46 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "AudioCallbackPlayTarget.h" +#include "AudioCallbackPlaySource.h" + +#include + +AudioCallbackPlayTarget::AudioCallbackPlayTarget(AudioCallbackPlaySource *source) : + m_source(source), + m_outputGain(1.0) +{ + if (m_source) { + connect(m_source, SIGNAL(modelReplaced()), + this, SLOT(sourceModelReplaced())); + } +} + +AudioCallbackPlayTarget::~AudioCallbackPlayTarget() +{ +} + +void +AudioCallbackPlayTarget::setOutputGain(float gain) +{ + m_outputGain = gain; +} + +#ifdef INCLUDE_MOCFILES +#ifdef INCLUDE_MOCFILES +#include "AudioCallbackPlayTarget.moc.cpp" +#endif +#endif + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioCallbackPlayTarget.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioCallbackPlayTarget.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,59 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _AUDIO_CALLBACK_PLAY_TARGET_H_ +#define _AUDIO_CALLBACK_PLAY_TARGET_H_ + +#include + +class AudioCallbackPlaySource; + +class AudioCallbackPlayTarget : public QObject +{ + Q_OBJECT + +public: + AudioCallbackPlayTarget(AudioCallbackPlaySource *source); + virtual ~AudioCallbackPlayTarget(); + + virtual bool isOK() const = 0; + + float getOutputGain() const { + return m_outputGain; + } + +public slots: + /** + * Set the playback gain (0.0 = silence, 1.0 = levels unmodified) + */ + virtual void setOutputGain(float gain); + + /** + * The main source model (providing the playback sample rate) has + * been changed. The target should query the source's sample + * rate, set its output sample rate accordingly, and call back on + * the source's setTargetSampleRate to indicate what sample rate + * it succeeded in setting at the output. If this differs from + * the model rate, the source will resample. + */ + virtual void sourceModelReplaced() = 0; + +protected: + AudioCallbackPlaySource *m_source; + float m_outputGain; +}; + +#endif + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioCoreAudioTarget.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioCoreAudioTarget.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,22 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifdef HAVE_COREAUDIO + +#include "AudioCoreAudioTarget.h" + + + +#endif diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioCoreAudioTarget.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioCoreAudioTarget.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,64 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _AUDIO_CORE_AUDIO_TARGET_H_ +#define _AUDIO_CORE_AUDIO_TARGET_H_ + +#ifdef HAVE_COREAUDIO + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "AudioCallbackPlayTarget.h" + +class AudioCallbackPlaySource; + +class AudioCoreAudioTarget : public AudioCallbackPlayTarget +{ + Q_OBJECT + +public: + AudioCoreAudioTarget(AudioCallbackPlaySource *source); + ~AudioCoreAudioTarget(); + + virtual bool isOK() const; + +public slots: + virtual void sourceModelReplaced(); + +protected: + OSStatus process(void *data, + AudioUnitRenderActionFlags *flags, + const AudioTimeStamp *timestamp, + unsigned int inbus, + unsigned int inframes, + AudioBufferList *ioData); + + int m_bufferSize; + int m_sampleRate; + int m_latency; +}; + +#endif /* HAVE_COREAUDIO */ + +#endif + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioGenerator.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioGenerator.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,764 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "AudioGenerator.h" + +#include "base/TempDirectory.h" +#include "base/PlayParameters.h" +#include "base/PlayParameterRepository.h" +#include "base/Pitch.h" +#include "base/Exceptions.h" + +#include "model/NoteModel.h" +#include "model/DenseTimeValueModel.h" +#include "model/SparseOneDimensionalModel.h" + +#include "plugin/RealTimePluginFactory.h" +#include "plugin/RealTimePluginInstance.h" +#include "plugin/PluginIdentifier.h" +#include "plugin/PluginXml.h" +#include "plugin/api/alsa/seq_event.h" + +#include +#include + +#include +#include + +const size_t +AudioGenerator::m_pluginBlockSize = 2048; + +QString +AudioGenerator::m_sampleDir = ""; + +//#define DEBUG_AUDIO_GENERATOR 1 + +AudioGenerator::AudioGenerator() : + m_sourceSampleRate(0), + m_targetChannelCount(1) +{ + connect(PlayParameterRepository::getInstance(), + SIGNAL(playPluginIdChanged(const Model *, QString)), + this, + SLOT(playPluginIdChanged(const Model *, QString))); + + connect(PlayParameterRepository::getInstance(), + SIGNAL(playPluginConfigurationChanged(const Model *, QString)), + this, + SLOT(playPluginConfigurationChanged(const Model *, QString))); +} + +AudioGenerator::~AudioGenerator() +{ +} + +bool +AudioGenerator::canPlay(const Model *model) +{ + if (dynamic_cast(model) || + dynamic_cast(model) || + dynamic_cast(model)) { + return true; + } else { + return false; + } +} + +bool +AudioGenerator::addModel(Model *model) +{ + if (m_sourceSampleRate == 0) { + + m_sourceSampleRate = model->getSampleRate(); + + } else { + + DenseTimeValueModel *dtvm = + dynamic_cast(model); + + if (dtvm) { + m_sourceSampleRate = model->getSampleRate(); + return true; + } + } + + RealTimePluginInstance *plugin = loadPluginFor(model); + if (plugin) { + QMutexLocker locker(&m_mutex); + m_synthMap[model] = plugin; + return true; + } + + return false; +} + +void +AudioGenerator::playPluginIdChanged(const Model *model, QString) +{ + if (m_synthMap.find(model) == m_synthMap.end()) return; + + RealTimePluginInstance *plugin = loadPluginFor(model); + if (plugin) { + QMutexLocker locker(&m_mutex); + delete m_synthMap[model]; + m_synthMap[model] = plugin; + } +} + +void +AudioGenerator::playPluginConfigurationChanged(const Model *model, + QString configurationXml) +{ +// std::cerr << "AudioGenerator::playPluginConfigurationChanged" << std::endl; + + if (m_synthMap.find(model) == m_synthMap.end()) { + std::cerr << "AudioGenerator::playPluginConfigurationChanged: We don't know about this plugin" << std::endl; + return; + } + + RealTimePluginInstance *plugin = m_synthMap[model]; + if (plugin) { + PluginXml(plugin).setParametersFromXml(configurationXml); + } +} + +QString +AudioGenerator::getDefaultPlayPluginId(const Model *model) +{ + const SparseOneDimensionalModel *sodm = + dynamic_cast(model); + if (sodm) { + return QString("dssi:%1:sample_player"). + arg(PluginIdentifier::BUILTIN_PLUGIN_SONAME); + } + + const NoteModel *nm = dynamic_cast(model); + if (nm) { + return QString("dssi:%1:sample_player"). + arg(PluginIdentifier::BUILTIN_PLUGIN_SONAME); + } + + return ""; +} + +QString +AudioGenerator::getDefaultPlayPluginConfiguration(const Model *model) +{ + QString program = ""; + + const SparseOneDimensionalModel *sodm = + dynamic_cast(model); + if (sodm) { + program = "tap"; + } + + const NoteModel *nm = dynamic_cast(model); + if (nm) { + program = "piano"; + } + + if (program == "") return ""; + + return + QString("") + .arg(XmlExportable::encodeEntities + (QString("sampledir=%1") + .arg(PluginXml::encodeConfigurationChars(getSampleDir())))) + .arg(XmlExportable::encodeEntities(program)); +} + +QString +AudioGenerator::getSampleDir() +{ + if (m_sampleDir != "") return m_sampleDir; + + try { + m_sampleDir = TempDirectory::getInstance()->getSubDirectoryPath("samples"); + } catch (DirectoryCreationFailed f) { + std::cerr << "WARNING: AudioGenerator::getSampleDir: Failed to create " + << "temporary sample directory" << std::endl; + m_sampleDir = ""; + return ""; + } + + QDir sampleResourceDir(":/samples", "*.wav"); + + for (unsigned int i = 0; i < sampleResourceDir.count(); ++i) { + + QString fileName(sampleResourceDir[i]); + QFile file(sampleResourceDir.filePath(fileName)); + + if (!file.copy(QDir(m_sampleDir).filePath(fileName))) { + std::cerr << "WARNING: AudioGenerator::getSampleDir: " + << "Unable to copy " << fileName.toStdString() + << " into temporary directory \"" + << m_sampleDir.toStdString() << "\"" << std::endl; + } + } + + return m_sampleDir; +} + +void +AudioGenerator::setSampleDir(RealTimePluginInstance *plugin) +{ + plugin->configure("sampledir", getSampleDir().toStdString()); +} + +RealTimePluginInstance * +AudioGenerator::loadPluginFor(const Model *model) +{ + QString pluginId, configurationXml; + + PlayParameters *parameters = + PlayParameterRepository::getInstance()->getPlayParameters(model); + if (parameters) { + pluginId = parameters->getPlayPluginId(); + configurationXml = parameters->getPlayPluginConfiguration(); + } + + if (pluginId == "") { + pluginId = getDefaultPlayPluginId(model); + configurationXml = getDefaultPlayPluginConfiguration(model); + } + + if (pluginId == "") return 0; + + RealTimePluginInstance *plugin = loadPlugin(pluginId, ""); + if (!plugin) return 0; + + if (configurationXml != "") { + PluginXml(plugin).setParametersFromXml(configurationXml); + } + + if (parameters) { + parameters->setPlayPluginId(pluginId); + parameters->setPlayPluginConfiguration(configurationXml); + } + + return plugin; +} + +RealTimePluginInstance * +AudioGenerator::loadPlugin(QString pluginId, QString program) +{ + RealTimePluginFactory *factory = + RealTimePluginFactory::instanceFor(pluginId); + + if (!factory) { + std::cerr << "Failed to get plugin factory" << std::endl; + return false; + } + + RealTimePluginInstance *instance = + factory->instantiatePlugin + (pluginId, 0, 0, m_sourceSampleRate, m_pluginBlockSize, m_targetChannelCount); + + if (!instance) { + std::cerr << "Failed to instantiate plugin " << pluginId.toStdString() << std::endl; + return 0; + } + + setSampleDir(instance); + + for (unsigned int i = 0; i < instance->getParameterCount(); ++i) { + instance->setParameterValue(i, instance->getParameterDefault(i)); + } + std::string defaultProgram = instance->getProgram(0, 0); + if (defaultProgram != "") { +// std::cerr << "first selecting default program " << defaultProgram << std::endl; + instance->selectProgram(defaultProgram); + } + if (program != "") { +// std::cerr << "now selecting desired program " << program.toStdString() << std::endl; + instance->selectProgram(program.toStdString()); + } + instance->setIdealChannelCount(m_targetChannelCount); // reset! + + return instance; +} + +void +AudioGenerator::removeModel(Model *model) +{ + SparseOneDimensionalModel *sodm = + dynamic_cast(model); + if (!sodm) return; // nothing to do + + QMutexLocker locker(&m_mutex); + + if (m_synthMap.find(sodm) == m_synthMap.end()) return; + + RealTimePluginInstance *instance = m_synthMap[sodm]; + m_synthMap.erase(sodm); + delete instance; +} + +void +AudioGenerator::clearModels() +{ + QMutexLocker locker(&m_mutex); + while (!m_synthMap.empty()) { + RealTimePluginInstance *instance = m_synthMap.begin()->second; + m_synthMap.erase(m_synthMap.begin()); + delete instance; + } +} + +void +AudioGenerator::reset() +{ + QMutexLocker locker(&m_mutex); + for (PluginMap::iterator i = m_synthMap.begin(); i != m_synthMap.end(); ++i) { + if (i->second) { + i->second->silence(); + i->second->discardEvents(); + } + } + + m_noteOffs.clear(); +} + +void +AudioGenerator::setTargetChannelCount(size_t targetChannelCount) +{ + if (m_targetChannelCount == targetChannelCount) return; + +// std::cerr << "AudioGenerator::setTargetChannelCount(" << targetChannelCount << ")" << std::endl; + + QMutexLocker locker(&m_mutex); + m_targetChannelCount = targetChannelCount; + + for (PluginMap::iterator i = m_synthMap.begin(); i != m_synthMap.end(); ++i) { + if (i->second) i->second->setIdealChannelCount(targetChannelCount); + } +} + +size_t +AudioGenerator::getBlockSize() const +{ + return m_pluginBlockSize; +} + +size_t +AudioGenerator::mixModel(Model *model, size_t startFrame, size_t frameCount, + float **buffer, size_t fadeIn, size_t fadeOut) +{ + if (m_sourceSampleRate == 0) { + std::cerr << "WARNING: AudioGenerator::mixModel: No base source sample rate available" << std::endl; + return frameCount; + } + + QMutexLocker locker(&m_mutex); + + PlayParameters *parameters = + PlayParameterRepository::getInstance()->getPlayParameters(model); + if (!parameters) return frameCount; + + bool playing = !parameters->isPlayMuted(); + if (!playing) return frameCount; + + float gain = parameters->getPlayGain(); + float pan = parameters->getPlayPan(); + + DenseTimeValueModel *dtvm = dynamic_cast(model); + if (dtvm) { + return mixDenseTimeValueModel(dtvm, startFrame, frameCount, + buffer, gain, pan, fadeIn, fadeOut); + } + + SparseOneDimensionalModel *sodm = dynamic_cast + (model); + if (sodm) { + return mixSparseOneDimensionalModel(sodm, startFrame, frameCount, + buffer, gain, pan, fadeIn, fadeOut); + } + + NoteModel *nm = dynamic_cast(model); + if (nm) { + return mixNoteModel(nm, startFrame, frameCount, + buffer, gain, pan, fadeIn, fadeOut); + } + + return frameCount; +} + +size_t +AudioGenerator::mixDenseTimeValueModel(DenseTimeValueModel *dtvm, + size_t startFrame, size_t frames, + float **buffer, float gain, float pan, + size_t fadeIn, size_t fadeOut) +{ + static float *channelBuffer = 0; + static size_t channelBufSiz = 0; + + size_t totalFrames = frames + fadeIn/2 + fadeOut/2; + + if (channelBufSiz < totalFrames) { + delete[] channelBuffer; + channelBuffer = new float[totalFrames]; + channelBufSiz = totalFrames; + } + + size_t got = 0; + size_t prevChannel = 999; + + for (size_t c = 0; c < m_targetChannelCount; ++c) { + + size_t sourceChannel = (c % dtvm->getChannelCount()); + +// std::cerr << "mixing channel " << c << " from source channel " << sourceChannel << std::endl; + + float channelGain = gain; + if (pan != 0.0) { + if (c == 0) { + if (pan > 0.0) channelGain *= 1.0 - pan; + } else { + if (pan < 0.0) channelGain *= pan + 1.0; + } + } + + if (prevChannel != sourceChannel) { + if (startFrame >= fadeIn/2) { + got = dtvm->getValues + (sourceChannel, + startFrame - fadeIn/2, startFrame + frames + fadeOut/2, + channelBuffer); + } else { + size_t missing = fadeIn/2 - startFrame; + got = dtvm->getValues + (sourceChannel, + 0, startFrame + frames + fadeOut/2, + channelBuffer + missing); + } + } + prevChannel = sourceChannel; + + for (size_t i = 0; i < fadeIn/2; ++i) { + float *back = buffer[c]; + back -= fadeIn/2; + back[i] += (channelGain * channelBuffer[i] * i) / fadeIn; + } + + for (size_t i = 0; i < frames + fadeOut/2; ++i) { + float mult = channelGain; + if (i < fadeIn/2) { + mult = (mult * i) / fadeIn; + } + if (i > frames - fadeOut/2) { + mult = (mult * ((frames + fadeOut/2) - i)) / fadeOut; + } + buffer[c][i] += mult * channelBuffer[i]; + } + } + + return got; +} + +size_t +AudioGenerator::mixSparseOneDimensionalModel(SparseOneDimensionalModel *sodm, + size_t startFrame, size_t frames, + float **buffer, float gain, float pan, + size_t /* fadeIn */, + size_t /* fadeOut */) +{ + RealTimePluginInstance *plugin = m_synthMap[sodm]; + if (!plugin) return 0; + + size_t latency = plugin->getLatency(); + size_t blocks = frames / m_pluginBlockSize; + + //!!! hang on -- the fact that the audio callback play source's + //buffer is a multiple of the plugin's buffer size doesn't mean + //that we always get called for a multiple of it here (because it + //also depends on the JACK block size). how should we ensure that + //all models write the same amount in to the mix, and that we + //always have a multiple of the plugin buffer size? I guess this + //class has to be queryable for the plugin buffer size & the + //callback play source has to use that as a multiple for all the + //calls to mixModel + + size_t got = blocks * m_pluginBlockSize; + +#ifdef DEBUG_AUDIO_GENERATOR + std::cout << "mixModel [sparse]: frames " << frames + << ", blocks " << blocks << std::endl; +#endif + + snd_seq_event_t onEv; + onEv.type = SND_SEQ_EVENT_NOTEON; + onEv.data.note.channel = 0; + onEv.data.note.note = 64; + onEv.data.note.velocity = 127; + + snd_seq_event_t offEv; + offEv.type = SND_SEQ_EVENT_NOTEOFF; + offEv.data.note.channel = 0; + offEv.data.note.velocity = 0; + + NoteOffSet ¬eOffs = m_noteOffs[sodm]; + + for (size_t i = 0; i < blocks; ++i) { + + size_t reqStart = startFrame + i * m_pluginBlockSize; + + SparseOneDimensionalModel::PointList points = + sodm->getPoints(reqStart + latency, + reqStart + latency + m_pluginBlockSize); + + Vamp::RealTime blockTime = Vamp::RealTime::frame2RealTime + (startFrame + i * m_pluginBlockSize, m_sourceSampleRate); + + for (SparseOneDimensionalModel::PointList::iterator pli = + points.begin(); pli != points.end(); ++pli) { + + size_t pliFrame = pli->frame; + + if (pliFrame >= latency) pliFrame -= latency; + + if (pliFrame < reqStart || + pliFrame >= reqStart + m_pluginBlockSize) continue; + + while (noteOffs.begin() != noteOffs.end() && + noteOffs.begin()->frame <= pliFrame) { + + Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime + (noteOffs.begin()->frame, m_sourceSampleRate); + + offEv.data.note.note = noteOffs.begin()->pitch; + +#ifdef DEBUG_AUDIO_GENERATOR + std::cerr << "mixModel [sparse]: sending note-off event at time " << eventTime << " frame " << noteOffs.begin()->frame << std::endl; +#endif + + plugin->sendEvent(eventTime, &offEv); + noteOffs.erase(noteOffs.begin()); + } + + Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime + (pliFrame, m_sourceSampleRate); + + plugin->sendEvent(eventTime, &onEv); + +#ifdef DEBUG_AUDIO_GENERATOR + std::cout << "mixModel [sparse]: point at frame " << pliFrame << ", block start " << (startFrame + i * m_pluginBlockSize) << ", resulting time " << eventTime << std::endl; +#endif + + size_t duration = 7000; // frames [for now] + NoteOff noff; + noff.pitch = onEv.data.note.note; + noff.frame = pliFrame + duration; + noteOffs.insert(noff); + } + + while (noteOffs.begin() != noteOffs.end() && + noteOffs.begin()->frame <= + startFrame + i * m_pluginBlockSize + m_pluginBlockSize) { + + Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime + (noteOffs.begin()->frame, m_sourceSampleRate); + + offEv.data.note.note = noteOffs.begin()->pitch; + +#ifdef DEBUG_AUDIO_GENERATOR + std::cerr << "mixModel [sparse]: sending leftover note-off event at time " << eventTime << " frame " << noteOffs.begin()->frame << std::endl; +#endif + + plugin->sendEvent(eventTime, &offEv); + noteOffs.erase(noteOffs.begin()); + } + + plugin->run(blockTime); + float **outs = plugin->getAudioOutputBuffers(); + + for (size_t c = 0; c < m_targetChannelCount; ++c) { +#ifdef DEBUG_AUDIO_GENERATOR + std::cout << "mixModel [sparse]: adding " << m_pluginBlockSize << " samples from plugin output " << c << std::endl; +#endif + + size_t sourceChannel = (c % plugin->getAudioOutputCount()); + + float channelGain = gain; + if (pan != 0.0) { + if (c == 0) { + if (pan > 0.0) channelGain *= 1.0 - pan; + } else { + if (pan < 0.0) channelGain *= pan + 1.0; + } + } + + for (size_t j = 0; j < m_pluginBlockSize; ++j) { + buffer[c][i * m_pluginBlockSize + j] += + channelGain * outs[sourceChannel][j]; + } + } + } + + return got; +} + + +//!!! mucho duplication with above -- refactor +size_t +AudioGenerator::mixNoteModel(NoteModel *nm, + size_t startFrame, size_t frames, + float **buffer, float gain, float pan, + size_t /* fadeIn */, + size_t /* fadeOut */) +{ + RealTimePluginInstance *plugin = m_synthMap[nm]; + if (!plugin) return 0; + + size_t latency = plugin->getLatency(); + size_t blocks = frames / m_pluginBlockSize; + + //!!! hang on -- the fact that the audio callback play source's + //buffer is a multiple of the plugin's buffer size doesn't mean + //that we always get called for a multiple of it here (because it + //also depends on the JACK block size). how should we ensure that + //all models write the same amount in to the mix, and that we + //always have a multiple of the plugin buffer size? I guess this + //class has to be queryable for the plugin buffer size & the + //callback play source has to use that as a multiple for all the + //calls to mixModel + + size_t got = blocks * m_pluginBlockSize; + +#ifdef DEBUG_AUDIO_GENERATOR + std::cout << "mixModel [note]: frames " << frames + << ", blocks " << blocks << std::endl; +#endif + + snd_seq_event_t onEv; + onEv.type = SND_SEQ_EVENT_NOTEON; + onEv.data.note.channel = 0; + onEv.data.note.note = 64; + onEv.data.note.velocity = 127; + + snd_seq_event_t offEv; + offEv.type = SND_SEQ_EVENT_NOTEOFF; + offEv.data.note.channel = 0; + offEv.data.note.velocity = 0; + + NoteOffSet ¬eOffs = m_noteOffs[nm]; + + for (size_t i = 0; i < blocks; ++i) { + + size_t reqStart = startFrame + i * m_pluginBlockSize; + + NoteModel::PointList points = + nm->getPoints(reqStart + latency, + reqStart + latency + m_pluginBlockSize); + + Vamp::RealTime blockTime = Vamp::RealTime::frame2RealTime + (startFrame + i * m_pluginBlockSize, m_sourceSampleRate); + + for (NoteModel::PointList::iterator pli = + points.begin(); pli != points.end(); ++pli) { + + size_t pliFrame = pli->frame; + + if (pliFrame >= latency) pliFrame -= latency; + + if (pliFrame < reqStart || + pliFrame >= reqStart + m_pluginBlockSize) continue; + + while (noteOffs.begin() != noteOffs.end() && + noteOffs.begin()->frame <= pliFrame) { + + Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime + (noteOffs.begin()->frame, m_sourceSampleRate); + + offEv.data.note.note = noteOffs.begin()->pitch; + +#ifdef DEBUG_AUDIO_GENERATOR + std::cerr << "mixModel [note]: sending note-off event at time " << eventTime << " frame " << noteOffs.begin()->frame << std::endl; +#endif + + plugin->sendEvent(eventTime, &offEv); + noteOffs.erase(noteOffs.begin()); + } + + Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime + (pliFrame, m_sourceSampleRate); + + if (nm->getScaleUnits() == "Hz") { + onEv.data.note.note = Pitch::getPitchForFrequency(pli->value); + } else { + onEv.data.note.note = lrintf(pli->value); + } + + plugin->sendEvent(eventTime, &onEv); + +#ifdef DEBUG_AUDIO_GENERATOR + std::cout << "mixModel [note]: point at frame " << pliFrame << ", block start " << (startFrame + i * m_pluginBlockSize) << ", resulting time " << eventTime << std::endl; +#endif + + size_t duration = pli->duration; + if (duration == 0 || duration == 1) { + duration = m_sourceSampleRate / 20; + } + NoteOff noff; + noff.pitch = onEv.data.note.note; + noff.frame = pliFrame + duration; + noteOffs.insert(noff); + } + + while (noteOffs.begin() != noteOffs.end() && + noteOffs.begin()->frame <= + startFrame + i * m_pluginBlockSize + m_pluginBlockSize) { + + Vamp::RealTime eventTime = Vamp::RealTime::frame2RealTime + (noteOffs.begin()->frame, m_sourceSampleRate); + + offEv.data.note.note = noteOffs.begin()->pitch; + +#ifdef DEBUG_AUDIO_GENERATOR + std::cerr << "mixModel [note]: sending leftover note-off event at time " << eventTime << " frame " << noteOffs.begin()->frame << std::endl; +#endif + + plugin->sendEvent(eventTime, &offEv); + noteOffs.erase(noteOffs.begin()); + } + + plugin->run(blockTime); + float **outs = plugin->getAudioOutputBuffers(); + + for (size_t c = 0; c < m_targetChannelCount; ++c) { +#ifdef DEBUG_AUDIO_GENERATOR + std::cout << "mixModel [note]: adding " << m_pluginBlockSize << " samples from plugin output " << c << std::endl; +#endif + + size_t sourceChannel = (c % plugin->getAudioOutputCount()); + + float channelGain = gain; + if (pan != 0.0) { + if (c == 0) { + if (pan > 0.0) channelGain *= 1.0 - pan; + } else { + if (pan < 0.0) channelGain *= pan + 1.0; + } + } + + for (size_t j = 0; j < m_pluginBlockSize; ++j) { + buffer[c][i * m_pluginBlockSize + j] += + channelGain * outs[sourceChannel][j]; + } + } + } + + return got; +} + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioGenerator.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioGenerator.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,144 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _AUDIO_GENERATOR_H_ +#define _AUDIO_GENERATOR_H_ + +class Model; +class NoteModel; +class DenseTimeValueModel; +class SparseOneDimensionalModel; +class RealTimePluginInstance; + +#include +#include + +#include +#include + +class AudioGenerator : public QObject +{ + Q_OBJECT + +public: + AudioGenerator(); + virtual ~AudioGenerator(); + + /** + * Return true if the given model is of a type that we generally + * know how to play. This doesn't guarantee that a specific + * AudioGenerator will actually produce sounds for it (for + * example, it may turn out that a vital plugin is missing). + */ + static bool canPlay(const Model *model); + + static QString getDefaultPlayPluginId(const Model *model); + static QString getDefaultPlayPluginConfiguration(const Model *model); + + /** + * Add a data model to be played from and initialise any necessary + * audio generation code. Returns true if the model will be + * played. (The return value test here is stricter than that for + * canPlay, above.) The model will be added regardless of the + * return value. + */ + virtual bool addModel(Model *model); + + /** + * Remove a model. + */ + virtual void removeModel(Model *model); + + /** + * Remove all models. + */ + virtual void clearModels(); + + /** + * Reset playback, clearing plugins and the like. + */ + virtual void reset(); + + /** + * Set the target channel count. The buffer parameter to mixModel + * must always point to at least this number of arrays. + */ + virtual void setTargetChannelCount(size_t channelCount); + + /** + * Return the internal processing block size. The frameCount + * argument to all mixModel calls must be a multiple of this + * value. + */ + virtual size_t getBlockSize() const; + + /** + * Mix a single model into an output buffer. + */ + virtual size_t mixModel(Model *model, size_t startFrame, size_t frameCount, + float **buffer, size_t fadeIn = 0, size_t fadeOut = 0); + +protected slots: + void playPluginIdChanged(const Model *, QString); + void playPluginConfigurationChanged(const Model *, QString); + +protected: + size_t m_sourceSampleRate; + size_t m_targetChannelCount; + + struct NoteOff { + + int pitch; + size_t frame; + + struct Comparator { + bool operator()(const NoteOff &n1, const NoteOff &n2) const { + return n1.frame < n2.frame; + } + }; + }; + + typedef std::map PluginMap; + + typedef std::set NoteOffSet; + typedef std::map NoteOffMap; + + QMutex m_mutex; + PluginMap m_synthMap; + NoteOffMap m_noteOffs; + static QString m_sampleDir; + + virtual RealTimePluginInstance *loadPluginFor(const Model *model); + virtual RealTimePluginInstance *loadPlugin(QString id, QString program); + static QString getSampleDir(); + static void setSampleDir(RealTimePluginInstance *plugin); + + virtual size_t mixDenseTimeValueModel + (DenseTimeValueModel *model, size_t startFrame, size_t frameCount, + float **buffer, float gain, float pan, size_t fadeIn, size_t fadeOut); + + virtual size_t mixSparseOneDimensionalModel + (SparseOneDimensionalModel *model, size_t startFrame, size_t frameCount, + float **buffer, float gain, float pan, size_t fadeIn, size_t fadeOut); + + virtual size_t mixNoteModel + (NoteModel *model, size_t startFrame, size_t frameCount, + float **buffer, float gain, float pan, size_t fadeIn, size_t fadeOut); + + static const size_t m_pluginBlockSize; +}; + +#endif + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioJACKTarget.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioJACKTarget.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,218 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifdef HAVE_JACK + +#include "AudioJACKTarget.h" +#include "AudioCallbackPlaySource.h" + +#include +#include + +//#define DEBUG_AUDIO_JACK_TARGET 1 + +AudioJACKTarget::AudioJACKTarget(AudioCallbackPlaySource *source) : + AudioCallbackPlayTarget(source), + m_client(0), + m_bufferSize(0), + m_sampleRate(0) +{ + char name[20]; + strcpy(name, "Sonic Visualiser"); + m_client = jack_client_new(name); + + if (!m_client) { + sprintf(name, "Sonic Visualiser (%d)", (int)getpid()); + m_client = jack_client_new(name); + if (!m_client) { + std::cerr + << "ERROR: AudioJACKTarget: Failed to connect to JACK server" + << std::endl; + } + } + + if (!m_client) return; + + m_bufferSize = jack_get_buffer_size(m_client); + m_sampleRate = jack_get_sample_rate(m_client); + + jack_set_process_callback(m_client, processStatic, this); + + if (jack_activate(m_client)) { + std::cerr << "ERROR: AudioJACKTarget: Failed to activate JACK client" + << std::endl; + } + + if (m_source) { + sourceModelReplaced(); + } +} + +AudioJACKTarget::~AudioJACKTarget() +{ + if (m_client) { + jack_deactivate(m_client); + jack_client_close(m_client); + } +} + +bool +AudioJACKTarget::isOK() const +{ + return (m_client != 0); +} + +int +AudioJACKTarget::processStatic(jack_nframes_t nframes, void *arg) +{ + return ((AudioJACKTarget *)arg)->process(nframes); +} + +void +AudioJACKTarget::sourceModelReplaced() +{ + m_mutex.lock(); + + m_source->setTargetBlockSize(m_bufferSize); + m_source->setTargetSampleRate(m_sampleRate); + + size_t channels = m_source->getSourceChannelCount(); + + // Because we offer pan, we always want at least 2 channels + if (channels < 2) channels = 2; + + if (channels == m_outputs.size() || !m_client) { + m_mutex.unlock(); + return; + } + + const char **ports = + jack_get_ports(m_client, NULL, NULL, + JackPortIsPhysical | JackPortIsInput); + size_t physicalPortCount = 0; + while (ports[physicalPortCount]) ++physicalPortCount; + +#ifdef DEBUG_AUDIO_JACK_TARGET + std::cerr << "AudioJACKTarget::sourceModelReplaced: have " << channels << " channels and " << physicalPortCount << " physical ports" << std::endl; +#endif + + while (m_outputs.size() < channels) { + + char name[20]; + jack_port_t *port; + + sprintf(name, "out %d", m_outputs.size() + 1); + + port = jack_port_register(m_client, + name, + JACK_DEFAULT_AUDIO_TYPE, + JackPortIsOutput, + 0); + + if (!port) { + std::cerr + << "ERROR: AudioJACKTarget: Failed to create JACK output port " + << m_outputs.size() << std::endl; + } else { + m_source->setTargetPlayLatency(jack_port_get_latency(port)); + } + + if (m_outputs.size() < physicalPortCount) { + jack_connect(m_client, jack_port_name(port), ports[m_outputs.size()]); + } + + m_outputs.push_back(port); + } + + while (m_outputs.size() > channels) { + std::vector::iterator itr = m_outputs.end(); + --itr; + jack_port_t *port = *itr; + if (port) jack_port_unregister(m_client, port); + m_outputs.erase(itr); + } + + m_mutex.unlock(); +} + +int +AudioJACKTarget::process(jack_nframes_t nframes) +{ + if (!m_mutex.tryLock()) { + return 0; + } + + if (m_outputs.empty()) { + m_mutex.unlock(); + return 0; + } + +#ifdef DEBUG_AUDIO_JACK_TARGET + std::cout << "AudioJACKTarget::process(" << nframes << "): have a source" << std::endl; +#endif + +#ifdef DEBUG_AUDIO_JACK_TARGET + if (m_bufferSize != nframes) { + std::cerr << "WARNING: m_bufferSize != nframes (" << m_bufferSize << " != " << nframes << ")" << std::endl; + } +#endif + + float **buffers = (float **)alloca(m_outputs.size() * sizeof(float *)); + + for (size_t ch = 0; ch < m_outputs.size(); ++ch) { + buffers[ch] = (float *)jack_port_get_buffer(m_outputs[ch], nframes); + } + + if (m_source) { + m_source->getSourceSamples(nframes, buffers); + } else { + for (size_t ch = 0; ch < m_outputs.size(); ++ch) { + for (size_t i = 0; i < nframes; ++i) { + buffers[ch][i] = 0.0; + } + } + } + + float peakLeft = 0.0, peakRight = 0.0; + + for (size_t ch = 0; ch < m_outputs.size(); ++ch) { + + float peak = 0.0; + + for (size_t i = 0; i < nframes; ++i) { + buffers[ch][i] *= m_outputGain; + float sample = fabsf(buffers[ch][i]); + if (sample > peak) peak = sample; + } + + if (ch == 0) peakLeft = peak; + if (ch > 0 || m_outputs.size() == 1) peakRight = peak; + } + + if (m_source) { + m_source->setOutputLevels(peakLeft, peakRight); + } + + m_mutex.unlock(); + return 0; +} + + +#ifdef INCLUDE_MOCFILES +#include "AudioJACKTarget.moc.cpp" +#endif + +#endif /* HAVE_JACK */ + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioJACKTarget.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioJACKTarget.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,58 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _AUDIO_JACK_TARGET_H_ +#define _AUDIO_JACK_TARGET_H_ + +#ifdef HAVE_JACK + +#include +#include + +#include "AudioCallbackPlayTarget.h" + +#include + +class AudioCallbackPlaySource; + +class AudioJACKTarget : public AudioCallbackPlayTarget +{ + Q_OBJECT + +public: + AudioJACKTarget(AudioCallbackPlaySource *source); + virtual ~AudioJACKTarget(); + + virtual bool isOK() const; + +public slots: + virtual void sourceModelReplaced(); + +protected: + int process(jack_nframes_t nframes); + + static int processStatic(jack_nframes_t, void *); + + jack_client_t *m_client; + std::vector m_outputs; + jack_nframes_t m_bufferSize; + jack_nframes_t m_sampleRate; + QMutex m_mutex; +}; + +#endif /* HAVE_JACK */ + +#endif + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioPortAudioTarget.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioPortAudioTarget.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,201 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifdef HAVE_PORTAUDIO + +#include "AudioPortAudioTarget.h" +#include "AudioCallbackPlaySource.h" + +#include +#include +#include + +//#define DEBUG_AUDIO_PORT_AUDIO_TARGET 1 + +AudioPortAudioTarget::AudioPortAudioTarget(AudioCallbackPlaySource *source) : + AudioCallbackPlayTarget(source), + m_stream(0), + m_bufferSize(0), + m_sampleRate(0), + m_latency(0) +{ + PaError err; + + err = Pa_Initialize(); + if (err != paNoError) { + std::cerr << "ERROR: AudioPortAudioTarget: Failed to initialize PortAudio" << std::endl; + return; + } + + m_bufferSize = 1024; + m_sampleRate = 44100; + if (m_source && (m_source->getSourceSampleRate() != 0)) { + m_sampleRate = m_source->getSourceSampleRate(); + } + + m_latency = Pa_GetMinNumBuffers(m_bufferSize, m_sampleRate) * m_bufferSize; + + std::cerr << "\n\n\nLATENCY= " << m_latency << std::endl; + + err = Pa_OpenDefaultStream(&m_stream, 0, 2, paFloat32, + m_sampleRate, m_bufferSize, 0, + processStatic, this); + + if (err != paNoError) { + std::cerr << "ERROR: AudioPortAudioTarget: Failed to open PortAudio stream" << std::endl; + m_stream = 0; + Pa_Terminate(); + return; + } + + err = Pa_StartStream(m_stream); + + if (err != paNoError) { + std::cerr << "ERROR: AudioPortAudioTarget: Failed to start PortAudio stream" << std::endl; + Pa_CloseStream(m_stream); + m_stream = 0; + Pa_Terminate(); + return; + } + + if (m_source) { + std::cerr << "AudioPortAudioTarget: block size " << m_bufferSize << std::endl; + m_source->setTargetBlockSize(m_bufferSize); + m_source->setTargetSampleRate(m_sampleRate); + m_source->setTargetPlayLatency(m_latency); + } +} + +AudioPortAudioTarget::~AudioPortAudioTarget() +{ + if (m_stream) { + PaError err; + err = Pa_CloseStream(m_stream); + if (err != paNoError) { + std::cerr << "ERROR: AudioPortAudioTarget: Failed to close PortAudio stream" << std::endl; + } + Pa_Terminate(); + } +} + +bool +AudioPortAudioTarget::isOK() const +{ + return (m_stream != 0); +} + +int +AudioPortAudioTarget::processStatic(void *input, void *output, + unsigned long nframes, + PaTimestamp outTime, void *data) +{ + return ((AudioPortAudioTarget *)data)->process(input, output, + nframes, outTime); +} + +void +AudioPortAudioTarget::sourceModelReplaced() +{ + m_source->setTargetSampleRate(m_sampleRate); +} + +int +AudioPortAudioTarget::process(void *inputBuffer, void *outputBuffer, + unsigned long nframes, + PaTimestamp) +{ +#ifdef DEBUG_AUDIO_PORT_AUDIO_TARGET + std::cout << "AudioPortAudioTarget::process(" << nframes << ")" << std::endl; +#endif + + if (!m_source) return 0; + + float *output = (float *)outputBuffer; + + assert(nframes <= m_bufferSize); + + static float **tmpbuf = 0; + static size_t tmpbufch = 0; + static size_t tmpbufsz = 0; + + size_t sourceChannels = m_source->getSourceChannelCount(); + + // Because we offer pan, we always want at least 2 channels + if (sourceChannels < 2) sourceChannels = 2; + + if (!tmpbuf || tmpbufch != sourceChannels || tmpbufsz < m_bufferSize) { + + if (tmpbuf) { + for (size_t i = 0; i < tmpbufch; ++i) { + delete[] tmpbuf[i]; + } + delete[] tmpbuf; + } + + tmpbufch = sourceChannels; + tmpbufsz = m_bufferSize; + tmpbuf = new float *[tmpbufch]; + + for (size_t i = 0; i < tmpbufch; ++i) { + tmpbuf[i] = new float[tmpbufsz]; + } + } + + m_source->getSourceSamples(nframes, tmpbuf); + + float peakLeft = 0.0, peakRight = 0.0; + + for (size_t ch = 0; ch < 2; ++ch) { + + float peak = 0.0; + + if (ch < sourceChannels) { + + // PortAudio samples are interleaved + for (size_t i = 0; i < nframes; ++i) { + output[i * 2 + ch] = tmpbuf[ch][i] * m_outputGain; + float sample = fabsf(output[i * 2 + ch]); + if (sample > peak) peak = sample; + } + + } else if (ch == 1 && sourceChannels == 1) { + + for (size_t i = 0; i < nframes; ++i) { + output[i * 2 + ch] = tmpbuf[0][i] * m_outputGain; + float sample = fabsf(output[i * 2 + ch]); + if (sample > peak) peak = sample; + } + + } else { + for (size_t i = 0; i < nframes; ++i) { + output[i * 2 + ch] = 0; + } + } + + if (ch == 0) peakLeft = peak; + if (ch > 0 || sourceChannels == 1) peakRight = peak; + } + + m_source->setOutputLevels(peakLeft, peakRight); + + return 0; +} + +#ifdef INCLUDE_MOCFILES +#include "AudioPortAudioTarget.moc.cpp" +#endif + +#endif /* HAVE_PORTAUDIO */ + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioPortAudioTarget.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioPortAudioTarget.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,58 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _AUDIO_PORT_AUDIO_TARGET_H_ +#define _AUDIO_PORT_AUDIO_TARGET_H_ + +#ifdef HAVE_PORTAUDIO + +#include +#include + +#include "AudioCallbackPlayTarget.h" + +class AudioCallbackPlaySource; + +class AudioPortAudioTarget : public AudioCallbackPlayTarget +{ + Q_OBJECT + +public: + AudioPortAudioTarget(AudioCallbackPlaySource *source); + virtual ~AudioPortAudioTarget(); + + virtual bool isOK() const; + +public slots: + virtual void sourceModelReplaced(); + +protected: + int process(void *input, void *output, unsigned long frames, + PaTimestamp outTime); + + static int processStatic(void *, void *, unsigned long, + PaTimestamp, void *); + + PortAudioStream *m_stream; + + int m_bufferSize; + int m_sampleRate; + int m_latency; +}; + +#endif /* HAVE_PORTAUDIO */ + +#endif + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioTargetFactory.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioTargetFactory.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,69 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "AudioTargetFactory.h" + +#include "AudioJACKTarget.h" +#include "AudioCoreAudioTarget.h" +#include "AudioPortAudioTarget.h" + +#include + +AudioCallbackPlayTarget * +AudioTargetFactory::createCallbackTarget(AudioCallbackPlaySource *source) +{ + AudioCallbackPlayTarget *target = 0; + +#ifdef HAVE_JACK + target = new AudioJACKTarget(source); + if (target->isOK()) return target; + else { + std::cerr << "WARNING: AudioTargetFactory::createCallbackTarget: Failed to open JACK target" << std::endl; + delete target; + } +#endif + +#ifdef HAVE_COREAUDIO + target = new AudioCoreAudioTarget(source); + if (target->isOK()) return target; + else { + std::cerr << "WARNING: AudioTargetFactory::createCallbackTarget: Failed to open CoreAudio target" << std::endl; + delete target; + } +#endif + +#ifdef HAVE_DIRECTSOUND + target = new AudioDirectSoundTarget(source); + if (target->isOK()) return target; + else { + std::cerr << "WARNING: AudioTargetFactory::createCallbackTarget: Failed to open DirectSound target" << std::endl; + delete target; + } +#endif + +#ifdef HAVE_PORTAUDIO + target = new AudioPortAudioTarget(source); + if (target->isOK()) return target; + else { + std::cerr << "WARNING: AudioTargetFactory::createCallbackTarget: Failed to open PortAudio target" << std::endl; + delete target; + } +#endif + + std::cerr << "WARNING: AudioTargetFactory::createCallbackTarget: No suitable targets available" << std::endl; + return 0; +} + + diff -r 000000000000 -r cd5d7ff8ef38 audioio/AudioTargetFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/AudioTargetFactory.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,29 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _AUDIO_TARGET_FACTORY_H_ +#define _AUDIO_TARGET_FACTORY_H_ + +class AudioCallbackPlaySource; +class AudioCallbackPlayTarget; + +class AudioTargetFactory +{ +public: + static AudioCallbackPlayTarget *createCallbackTarget(AudioCallbackPlaySource *); +}; + +#endif + diff -r 000000000000 -r cd5d7ff8ef38 audioio/IntegerTimeStretcher.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/IntegerTimeStretcher.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,226 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "IntegerTimeStretcher.h" + +#include +#include + +//#define DEBUG_INTEGER_TIME_STRETCHER 1 + +IntegerTimeStretcher::IntegerTimeStretcher(size_t ratio, + size_t maxProcessInputBlockSize, + size_t inputIncrement, + size_t windowSize, + WindowType windowType) : + m_ratio(ratio), + m_n1(inputIncrement), + m_n2(m_n1 * ratio), + m_wlen(std::max(windowSize, m_n2 * 2)), + m_inbuf(m_wlen), + m_outbuf(maxProcessInputBlockSize * ratio) +{ + m_window = new Window(windowType, m_wlen), + + m_time = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * m_wlen); + m_freq = (fftwf_complex *)fftwf_malloc(sizeof(fftwf_complex) * m_wlen); + m_dbuf = (float *)fftwf_malloc(sizeof(float) * m_wlen); + + m_plan = fftwf_plan_dft_1d(m_wlen, m_time, m_freq, FFTW_FORWARD, FFTW_ESTIMATE); + m_iplan = fftwf_plan_dft_c2r_1d(m_wlen, m_freq, m_dbuf, FFTW_ESTIMATE); + + m_mashbuf = new float[m_wlen]; + for (int i = 0; i < m_wlen; ++i) { + m_mashbuf[i] = 0.0; + } +} + +IntegerTimeStretcher::~IntegerTimeStretcher() +{ + std::cerr << "IntegerTimeStretcher::~IntegerTimeStretcher" << std::endl; + + fftwf_destroy_plan(m_plan); + fftwf_destroy_plan(m_iplan); + + fftwf_free(m_time); + fftwf_free(m_freq); + fftwf_free(m_dbuf); + + delete m_window; + delete m_mashbuf; +} + +size_t +IntegerTimeStretcher::getProcessingLatency() const +{ + return getWindowSize() - getInputIncrement(); +} + +void +IntegerTimeStretcher::process(float *input, float *output, size_t samples) +{ + // We need to add samples from input to our internal buffer. When + // we have m_windowSize samples in the buffer, we can process it, + // move the samples back by m_n1 and write the output onto our + // internal output buffer. If we have (samples * ratio) samples + // in that, we can write m_n2 of them back to output and return + // (otherwise we have to write zeroes). + + // When we process, we write m_wlen to our fixed output buffer + // (m_mashbuf). We then pull out the first m_n2 samples from that + // buffer, push them into the output ring buffer, and shift + // m_mashbuf left by that amount. + + // The processing latency is then m_wlen - m_n2. + + size_t consumed = 0; + +#ifdef DEBUG_INTEGER_TIME_STRETCHER + std::cerr << "IntegerTimeStretcher::process(" << samples << ", consumed = " << consumed << "), writable " << m_inbuf.getWriteSpace() <<", readable "<< m_outbuf.getReadSpace() << std::endl; +#endif + + while (consumed < samples) { + + size_t writable = m_inbuf.getWriteSpace(); + writable = std::min(writable, samples - consumed); + + if (writable == 0) { + //!!! then what? I don't think this should happen, but + std::cerr << "WARNING: IntegerTimeStretcher::process: writable == 0" << std::endl; + break; + } + +#ifdef DEBUG_INTEGER_TIME_STRETCHER + std::cerr << "writing " << writable << " from index " << consumed << " to inbuf, consumed will be " << consumed + writable << std::endl; +#endif + m_inbuf.write(input + consumed, writable); + consumed += writable; + + while (m_inbuf.getReadSpace() >= m_wlen && + m_outbuf.getWriteSpace() >= m_n2) { + + // We know we have at least m_wlen samples available + // in m_inbuf. We need to peek m_wlen of them for + // processing, and then read m_n1 to advance the read + // pointer. + + size_t got = m_inbuf.peek(m_dbuf, m_wlen); + assert(got == m_wlen); + + processBlock(m_dbuf, m_mashbuf); + +#ifdef DEBUG_INTEGER_TIME_STRETCHER + std::cerr << "writing first " << m_n2 << " from mashbuf, skipping " << m_n1 << " on inbuf " << std::endl; +#endif + m_inbuf.skip(m_n1); + m_outbuf.write(m_mashbuf, m_n2); + + for (size_t i = 0; i < m_wlen - m_n2; ++i) { + m_mashbuf[i] = m_mashbuf[i + m_n2]; + } + for (size_t i = m_wlen - m_n2; i < m_wlen; ++i) { + m_mashbuf[i] = 0.0f; + } + } + +// std::cerr << "WARNING: IntegerTimeStretcher::process: writespace not enough for output increment (" << m_outbuf.getWriteSpace() << " < " << m_n2 << ")" << std::endl; +// } + +#ifdef DEBUG_INTEGER_TIME_STRETCHER + std::cerr << "loop ended: inbuf read space " << m_inbuf.getReadSpace() << ", outbuf write space " << m_outbuf.getWriteSpace() << std::endl; +#endif + } + + if (m_outbuf.getReadSpace() < samples * m_ratio) { + std::cerr << "WARNING: IntegerTimeStretcher::process: not enough data (yet?) (" << m_outbuf.getReadSpace() << " < " << (samples * m_ratio) << ")" << std::endl; + size_t fill = samples * m_ratio - m_outbuf.getReadSpace(); + for (size_t i = 0; i < fill; ++i) { + output[i] = 0.0; + } + m_outbuf.read(output + fill, m_outbuf.getReadSpace()); + } else { +#ifdef DEBUG_INTEGER_TIME_STRETCHER + std::cerr << "enough data - writing " << samples * m_ratio << " from outbuf" << std::endl; +#endif + m_outbuf.read(output, samples * m_ratio); + } + +#ifdef DEBUG_INTEGER_TIME_STRETCHER + std::cerr << "IntegerTimeStretcher::process returning" << std::endl; +#endif +} + +void +IntegerTimeStretcher::processBlock(float *buf, float *out) +{ + size_t i; + + // buf contains m_wlen samples; out contains enough space for + // m_wlen * ratio samples (we mix into out, rather than replacing) + +#ifdef DEBUG_INTEGER_TIME_STRETCHER + std::cerr << "IntegerTimeStretcher::processBlock" << std::endl; +#endif + + m_window->cut(buf); + + for (i = 0; i < m_wlen/2; ++i) { + float temp = buf[i]; + buf[i] = buf[i + m_wlen/2]; + buf[i + m_wlen/2] = temp; + } + + for (i = 0; i < m_wlen; ++i) { + m_time[i][0] = buf[i]; + m_time[i][1] = 0.0; + } + + fftwf_execute(m_plan); // m_time -> m_freq + + for (i = 0; i < m_wlen; ++i) { + + float mag = sqrtf(m_freq[i][0] * m_freq[i][0] + + m_freq[i][1] * m_freq[i][1]); + + float phase = atan2f(m_freq[i][1], m_freq[i][0]); + + phase = phase * m_ratio; + + float real = mag * cosf(phase); + float imag = mag * sinf(phase); + m_freq[i][0] = real; + m_freq[i][1] = imag; + } + + fftwf_execute(m_iplan); // m_freq -> in, inverse fft + + for (i = 0; i < m_wlen/2; ++i) { + float temp = buf[i] / m_wlen; + buf[i] = buf[i + m_wlen/2] / m_wlen; + buf[i + m_wlen/2] = temp; + } + + m_window->cut(buf); + + int div = m_wlen / m_n2; + if (div > 1) div /= 2; + for (i = 0; i < m_wlen; ++i) { + buf[i] /= div; + } + + for (i = 0; i < m_wlen; ++i) { + out[i] += buf[i]; + } +} diff -r 000000000000 -r cd5d7ff8ef38 audioio/IntegerTimeStretcher.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/audioio/IntegerTimeStretcher.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,91 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _INTEGER_TIME_STRETCHER_H_ +#define _INTEGER_TIME_STRETCHER_H_ + +#include "base/Window.h" +#include "base/RingBuffer.h" + +#include + +/** + * A time stretcher that slows down audio by an integer multiple of + * its original duration, preserving pitch. This uses the simple + * phase vocoder technique from DAFX pp275-276, adding a block-based + * stream oriented API. + * + * Causes significant transient smearing, but sounds good for steady + * notes and is generally predictable. + */ + +class IntegerTimeStretcher +{ +public: + IntegerTimeStretcher(size_t ratio, + size_t maxProcessInputBlockSize, + size_t inputIncrement = 64, + size_t windowSize = 2048, + WindowType windowType = HanningWindow); + virtual ~IntegerTimeStretcher(); + + void process(float *input, float *output, size_t samples); + + /** + * Get the hop size for input. Smaller values may produce better + * results, at a cost in processing time. Larger values are + * faster but increase the likelihood of echo-like effects. The + * default is 64, which is usually pretty good, though heavy on + * processor power. + */ + size_t getInputIncrement() const { return m_n1; } + + /** + * Get the window size for FFT processing. Must be larger than + * the input and output increments. The default is 2048. + */ + size_t getWindowSize() const { return m_wlen; } + + /** + * Get the window type. The default is a Hanning window. + */ + WindowType getWindowType() const { return m_window->getType(); } + + size_t getRatio() const { return m_ratio; } + size_t getOutputIncrement() const { return getInputIncrement() * getRatio(); } + size_t getProcessingLatency() const; + +protected: + void processBlock(float *in, float *out); + + size_t m_ratio; + size_t m_n1; + size_t m_n2; + size_t m_wlen; + Window *m_window; + + fftwf_complex *m_time; + fftwf_complex *m_freq; + float *m_dbuf; + + fftwf_plan m_plan; + fftwf_plan m_iplan; + + RingBuffer m_inbuf; + RingBuffer m_outbuf; + float *m_mashbuf; +}; + +#endif diff -r 000000000000 -r cd5d7ff8ef38 document/Document.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/document/Document.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,746 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "Document.h" + +#include "model/WaveFileModel.h" +#include "base/Layer.h" +#include "base/CommandHistory.h" +#include "base/Command.h" +#include "base/View.h" +#include "base/PlayParameterRepository.h" +#include "base/PlayParameters.h" +#include "transform/TransformFactory.h" +#include + +//!!! still need to handle command history, documentRestored/documentModified + +Document::Document() : + m_mainModel(0) +{ +} + +Document::~Document() +{ + //!!! Document should really own the command history. atm we + //still refer to it in various places that don't have access to + //the document, be nice to fix that + +// std::cerr << "\n\nDocument::~Document: about to clear command history" << std::endl; + CommandHistory::getInstance()->clear(); + +// std::cerr << "Document::~Document: about to delete layers" << std::endl; + while (!m_layers.empty()) { + deleteLayer(*m_layers.begin(), true); + } + + if (!m_models.empty()) { + std::cerr << "Document::~Document: WARNING: " + << m_models.size() << " model(s) still remain -- " + << "should have been garbage collected when deleting layers" + << std::endl; + while (!m_models.empty()) { + if (m_models.begin()->first == m_mainModel) { + // just in case! + std::cerr << "Document::~Document: WARNING: Main model is also" + << " in models list!" << std::endl; + } else { + emit modelAboutToBeDeleted(m_models.begin()->first); + delete m_models.begin()->first; + } + m_models.erase(m_models.begin()); + } + } + +// std::cerr << "Document::~Document: About to get rid of main model" +// << std::endl; + emit modelAboutToBeDeleted(m_mainModel); + emit mainModelChanged(0); + delete m_mainModel; + +} + +Layer * +Document::createLayer(LayerFactory::LayerType type) +{ + Layer *newLayer = LayerFactory::getInstance()->createLayer(type); + if (!newLayer) return 0; + + newLayer->setObjectName(getUniqueLayerName(newLayer->objectName())); + + m_layers.insert(newLayer); + emit layerAdded(newLayer); + + return newLayer; +} + +Layer * +Document::createMainModelLayer(LayerFactory::LayerType type) +{ + Layer *newLayer = createLayer(type); + if (!newLayer) return 0; + setModel(newLayer, m_mainModel); + return newLayer; +} + +Layer * +Document::createImportedLayer(Model *model) +{ + LayerFactory::LayerTypeSet types = + LayerFactory::getInstance()->getValidLayerTypes(model); + + if (types.empty()) { + std::cerr << "WARNING: Document::importLayer: no valid display layer for model" << std::endl; + return 0; + } + + //!!! for now, just use the first suitable layer type + LayerFactory::LayerType type = *types.begin(); + + Layer *newLayer = LayerFactory::getInstance()->createLayer(type); + if (!newLayer) return 0; + + newLayer->setObjectName(getUniqueLayerName(newLayer->objectName())); + + addImportedModel(model); + setModel(newLayer, model); + + //!!! and all channels + setChannel(newLayer, -1); + + m_layers.insert(newLayer); + emit layerAdded(newLayer); + return newLayer; +} + +Layer * +Document::createEmptyLayer(LayerFactory::LayerType type) +{ + Model *newModel = + LayerFactory::getInstance()->createEmptyModel(type, m_mainModel); + if (!newModel) return 0; + + Layer *newLayer = createLayer(type); + if (!newLayer) { + delete newModel; + return 0; + } + + addImportedModel(newModel); + setModel(newLayer, newModel); + + return newLayer; +} + +Layer * +Document::createDerivedLayer(LayerFactory::LayerType type, + TransformName transform) +{ + Layer *newLayer = createLayer(type); + if (!newLayer) return 0; + + newLayer->setObjectName(getUniqueLayerName + (TransformFactory::getInstance()-> + getTransformFriendlyName(transform))); + + return newLayer; +} + +Layer * +Document::createDerivedLayer(TransformName transform, + Model *inputModel, + int channel, + QString configurationXml) +{ + Model *newModel = createModelForTransform(transform, inputModel, + channel, configurationXml); + if (!newModel) { + // error already printed to stderr by createModelForTransform + emit modelGenerationFailed(transform); + return 0; + } + + LayerFactory::LayerTypeSet types = + LayerFactory::getInstance()->getValidLayerTypes(newModel); + + if (types.empty()) { + std::cerr << "WARNING: Document::createLayerForTransform: no valid display layer for output of transform " << transform.toStdString() << std::endl; + delete newModel; + return 0; + } + + //!!! for now, just use the first suitable layer type + + Layer *newLayer = createLayer(*types.begin()); + setModel(newLayer, newModel); + + //!!! We need to clone the model when adding the layer, so that it + //can be edited without affecting other layers that are based on + //the same model. Unfortunately we can't just clone it now, + //because it probably hasn't been completed yet -- the transform + //runs in the background. Maybe the transform has to handle + //cloning and cacheing models itself. + // + // Once we do clone models here, of course, we'll have to avoid + // leaking them too. + // + // We want the user to be able to add a model to a second layer + // _while it's still being calculated in the first_ and have it + // work quickly. That means we need to put the same physical + // model pointer in both layers, so they can't actually be cloned. + + if (newLayer) { + newLayer->setObjectName(getUniqueLayerName + (TransformFactory::getInstance()-> + getTransformFriendlyName(transform))); + } + + emit layerAdded(newLayer); + return newLayer; +} + +void +Document::setMainModel(WaveFileModel *model) +{ + Model *oldMainModel = m_mainModel; + m_mainModel = model; + + emit modelAdded(m_mainModel); + + std::vector obsoleteLayers; + std::set failedTransforms; + + // We need to ensure that no layer is left using oldMainModel or + // any of the old derived models as its model. Either replace the + // model, or delete the layer for each layer that is currently + // using one of these. Carry out this replacement before we + // delete any of the models. + + for (LayerSet::iterator i = m_layers.begin(); i != m_layers.end(); ++i) { + + Layer *layer = *i; + Model *model = layer->getModel(); + + if (model == oldMainModel) { + LayerFactory::getInstance()->setModel(layer, m_mainModel); + continue; + } + + if (m_models.find(model) == m_models.end()) { + std::cerr << "WARNING: Document::setMainModel: Unknown model " + << model << " in layer " << layer << std::endl; + // get rid of this hideous degenerate + obsoleteLayers.push_back(layer); + continue; + } + + if (m_models[model].source == oldMainModel) { + + // This model was derived from the previous main + // model: regenerate it. + + TransformName transform = m_models[model].transform; + int channel = m_models[model].channel; + + Model *replacementModel = + createModelForTransform(transform, + m_mainModel, + channel, + m_models[model].configurationXml); + + if (!replacementModel) { + std::cerr << "WARNING: Document::setMainModel: Failed to regenerate model for transform \"" + << transform.toStdString() << "\"" << " in layer " << layer << std::endl; + if (failedTransforms.find(transform) == failedTransforms.end()) { + emit modelRegenerationFailed(layer->objectName(), + transform); + failedTransforms.insert(transform); + } + obsoleteLayers.push_back(layer); + } else { + setModel(layer, replacementModel); + } + } + } + + for (size_t k = 0; k < obsoleteLayers.size(); ++k) { + deleteLayer(obsoleteLayers[k], true); + } + + emit mainModelChanged(m_mainModel); + + // we already emitted modelAboutToBeDeleted for this + delete oldMainModel; +} + +void +Document::addDerivedModel(TransformName transform, + Model *inputModel, + int channel, + Model *outputModelToAdd, + QString configurationXml) +{ + if (m_models.find(outputModelToAdd) != m_models.end()) { + std::cerr << "WARNING: Document::addDerivedModel: Model already added" + << std::endl; + return; + } + + ModelRecord rec; + rec.source = inputModel; + rec.transform = transform; + rec.channel = channel; + rec.configurationXml = configurationXml; + rec.refcount = 0; + + m_models[outputModelToAdd] = rec; + + emit modelAdded(outputModelToAdd); +} + + +void +Document::addImportedModel(Model *model) +{ + if (m_models.find(model) != m_models.end()) { + std::cerr << "WARNING: Document::addImportedModel: Model already added" + << std::endl; + return; + } + + ModelRecord rec; + rec.source = 0; + rec.transform = ""; + rec.channel = -1; + rec.refcount = 0; + + m_models[model] = rec; + + emit modelAdded(model); +} + +Model * +Document::createModelForTransform(TransformName transform, + Model *inputModel, + int channel, + QString configurationXml) +{ + Model *model = 0; + + for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) { + if (i->second.transform == transform && + i->second.source == inputModel && + i->second.channel == channel && + i->second.configurationXml == configurationXml) { + return i->first; + } + } + + model = TransformFactory::getInstance()->transform + (transform, inputModel, channel, configurationXml); + + if (!model) { + std::cerr << "WARNING: Document::createModelForTransform: no output model for transform " << transform.toStdString() << std::endl; + } else { + addDerivedModel(transform, inputModel, channel, model, configurationXml); + } + + return model; +} + +void +Document::releaseModel(Model *model) // Will _not_ release main model! +{ + if (model == 0) { + return; + } + + if (model == m_mainModel) { + return; + } + + bool toDelete = false; + + if (m_models.find(model) != m_models.end()) { + + if (m_models[model].refcount == 0) { + std::cerr << "WARNING: Document::releaseModel: model " << model + << " reference count is zero already!" << std::endl; + } else { + if (--m_models[model].refcount == 0) { + toDelete = true; + } + } + } else { + std::cerr << "WARNING: Document::releaseModel: Unfound model " + << model << std::endl; + toDelete = true; + } + + if (toDelete) { + + int sourceCount = 0; + + for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) { + if (i->second.source == model) { + ++sourceCount; + i->second.source = 0; + } + } + + if (sourceCount > 0) { + std::cerr << "Document::releaseModel: Deleting model " + << model << " even though it is source for " + << sourceCount << " other derived model(s) -- resetting " + << "their source fields appropriately" << std::endl; + } + + emit modelAboutToBeDeleted(model); + m_models.erase(model); + delete model; + } +} + +void +Document::deleteLayer(Layer *layer, bool force) +{ + if (m_layerViewMap.find(layer) != m_layerViewMap.end() && + m_layerViewMap[layer].size() > 0) { + + std::cerr << "WARNING: Document::deleteLayer: Layer " + << layer << " [" << layer->objectName().toStdString() << "]" + << " is still used in " << m_layerViewMap[layer].size() + << " views!" << std::endl; + + if (force) { + + std::cerr << "(force flag set -- deleting from all views)" << std::endl; + + for (std::set::iterator j = m_layerViewMap[layer].begin(); + j != m_layerViewMap[layer].end(); ++j) { + // don't use removeLayerFromView, as it issues a command + layer->setLayerDormant(*j, true); + (*j)->removeLayer(layer); + } + + m_layerViewMap.erase(layer); + + } else { + return; + } + } + + if (m_layers.find(layer) == m_layers.end()) { + std::cerr << "Document::deleteLayer: Layer " + << layer << " does not exist, or has already been deleted " + << "(this may not be as serious as it sounds)" << std::endl; + return; + } + + m_layers.erase(layer); + + releaseModel(layer->getModel()); + emit layerRemoved(layer); + emit layerAboutToBeDeleted(layer); + delete layer; +} + +void +Document::setModel(Layer *layer, Model *model) +{ + if (model && + model != m_mainModel && + m_models.find(model) == m_models.end()) { + std::cerr << "ERROR: Document::setModel: Layer " << layer + << " is using unregistered model " << model + << ": register the layer's model before setting it!" + << std::endl; + return; + } + + if (layer->getModel()) { + if (layer->getModel() == model) { + std::cerr << "WARNING: Document::setModel: Layer is already set to this model" << std::endl; + return; + } + releaseModel(layer->getModel()); + } + + if (model && model != m_mainModel) { + m_models[model].refcount ++; + } + + LayerFactory::getInstance()->setModel(layer, model); +} + +void +Document::setChannel(Layer *layer, int channel) +{ + LayerFactory::getInstance()->setChannel(layer, channel); +} + +void +Document::addLayerToView(View *view, Layer *layer) +{ + Model *model = layer->getModel(); + if (!model) { + std::cerr << "Document::addLayerToView: Layer with no model being added to view: normally you want to set the model first" << std::endl; + } else { + if (model != m_mainModel && + m_models.find(model) == m_models.end()) { + std::cerr << "ERROR: Document::addLayerToView: Layer " << layer + << " has unregistered model " << model + << " -- register the layer's model before adding the layer!" << std::endl; + return; + } + } + + CommandHistory::getInstance()->addCommand + (new Document::AddLayerCommand(this, view, layer)); +} + +void +Document::removeLayerFromView(View *view, Layer *layer) +{ + CommandHistory::getInstance()->addCommand + (new Document::RemoveLayerCommand(this, view, layer)); +} + +void +Document::addToLayerViewMap(Layer *layer, View *view) +{ + bool firstView = (m_layerViewMap.find(layer) == m_layerViewMap.end() || + m_layerViewMap[layer].empty()); + + if (m_layerViewMap[layer].find(view) != + m_layerViewMap[layer].end()) { + std::cerr << "WARNING: Document::addToLayerViewMap:" + << " Layer " << layer << " -> view " << view << " already in" + << " layer view map -- internal inconsistency" << std::endl; + } + + m_layerViewMap[layer].insert(view); + + if (firstView) emit layerInAView(layer, true); +} + +void +Document::removeFromLayerViewMap(Layer *layer, View *view) +{ + if (m_layerViewMap[layer].find(view) == + m_layerViewMap[layer].end()) { + std::cerr << "WARNING: Document::removeFromLayerViewMap:" + << " Layer " << layer << " -> view " << view << " not in" + << " layer view map -- internal inconsistency" << std::endl; + } + + m_layerViewMap[layer].erase(view); + + if (m_layerViewMap[layer].empty()) { + m_layerViewMap.erase(layer); + emit layerInAView(layer, false); + } +} + +QString +Document::getUniqueLayerName(QString candidate) +{ + for (int count = 1; ; ++count) { + + QString adjusted = + (count > 1 ? QString("%1 <%2>").arg(candidate).arg(count) : + candidate); + + bool duplicate = false; + + for (LayerSet::iterator i = m_layers.begin(); i != m_layers.end(); ++i) { + if ((*i)->objectName() == adjusted) { + duplicate = true; + break; + } + } + + if (!duplicate) return adjusted; + } +} + +Document::AddLayerCommand::AddLayerCommand(Document *d, + View *view, + Layer *layer) : + m_d(d), + m_view(view), + m_layer(layer), + m_name(d->tr("Add %1 Layer").arg(layer->objectName())), + m_added(false) +{ +} + +Document::AddLayerCommand::~AddLayerCommand() +{ +// std::cerr << "Document::AddLayerCommand::~AddLayerCommand" << std::endl; + if (!m_added) { + m_d->deleteLayer(m_layer); + } +} + +void +Document::AddLayerCommand::execute() +{ + for (int i = 0; i < m_view->getLayerCount(); ++i) { + if (m_view->getLayer(i) == m_layer) { + // already there + m_layer->setLayerDormant(m_view, false); + m_added = true; + return; + } + } + + m_view->addLayer(m_layer); + m_layer->setLayerDormant(m_view, false); + + m_d->addToLayerViewMap(m_layer, m_view); + m_added = true; +} + +void +Document::AddLayerCommand::unexecute() +{ + m_view->removeLayer(m_layer); + m_layer->setLayerDormant(m_view, true); + + m_d->removeFromLayerViewMap(m_layer, m_view); + m_added = false; +} + +Document::RemoveLayerCommand::RemoveLayerCommand(Document *d, + View *view, + Layer *layer) : + m_d(d), + m_view(view), + m_layer(layer), + m_name(d->tr("Delete %1 Layer").arg(layer->objectName())), + m_added(true) +{ +} + +Document::RemoveLayerCommand::~RemoveLayerCommand() +{ +// std::cerr << "Document::RemoveLayerCommand::~RemoveLayerCommand" << std::endl; + if (!m_added) { + m_d->deleteLayer(m_layer); + } +} + +void +Document::RemoveLayerCommand::execute() +{ + bool have = false; + for (int i = 0; i < m_view->getLayerCount(); ++i) { + if (m_view->getLayer(i) == m_layer) { + have = true; + break; + } + } + + if (!have) { // not there! + m_layer->setLayerDormant(m_view, true); + m_added = false; + return; + } + + m_view->removeLayer(m_layer); + m_layer->setLayerDormant(m_view, true); + + m_d->removeFromLayerViewMap(m_layer, m_view); + m_added = false; +} + +void +Document::RemoveLayerCommand::unexecute() +{ + m_view->addLayer(m_layer); + m_layer->setLayerDormant(m_view, false); + + m_d->addToLayerViewMap(m_layer, m_view); + m_added = true; +} + +void +Document::toXml(QTextStream &out, QString indent, QString extraAttributes) const +{ + out << indent + QString("\n") + .arg(extraAttributes == "" ? "" : " ").arg(extraAttributes); + + if (m_mainModel) { + m_mainModel->toXml(out, indent + " ", "mainModel=\"true\""); + } + + for (ModelMap::const_iterator i = m_models.begin(); + i != m_models.end(); ++i) { + + i->first->toXml(out, indent + " "); + + const ModelRecord &rec = i->second; + + if (rec.source && rec.transform != "") { + + out << indent; + out << QString(" first)) + .arg(rec.channel) + .arg(XmlExportable::encodeEntities(rec.transform)); + + if (rec.configurationXml != "") { + out << ">\n " + indent + rec.configurationXml + + "\n" + indent + " \n"; + } else { + out << "/>\n"; + } + } + + //!!! We should probably own the PlayParameterRepository + PlayParameters *playParameters = + PlayParameterRepository::getInstance()->getPlayParameters(i->first); + if (playParameters) { + playParameters->toXml + (out, indent + " ", + QString("model=\"%1\"") + .arg(XmlExportable::getObjectExportId(i->first))); + } + } + + for (LayerSet::const_iterator i = m_layers.begin(); + i != m_layers.end(); ++i) { + + (*i)->toXml(out, indent + " "); + } + + out << indent + "\n"; +} + +QString +Document::toXmlString(QString indent, QString extraAttributes) const +{ + QString s; + + { + QTextStream out(&s); + toXml(out, indent, extraAttributes); + } + + return s; +} + diff -r 000000000000 -r cd5d7ff8ef38 document/Document.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/document/Document.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,295 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _DOCUMENT_H_ +#define _DOCUMENT_H_ + +#include "layer/LayerFactory.h" +#include "transform/Transform.h" +#include "base/Command.h" + +#include +#include + +class Model; +class Layer; +class View; +class WaveFileModel; + +/** + * A Sonic Visualiser document consists of a set of data models, and + * also the visualisation layers used to display them. Changes to the + * layers and their layout need to be stored and managed in much the + * same way as changes to the underlying data. + * + * The document manages: + * + * -- A main data model, which provides the underlying sample rate and + * such like. This must be a wave file model. + * + * -- Any number of imported models, which contain data without any + * requirement to remember where the data came from or how to + * regenerate it. + * + * -- Any number of models generated by transforms such as feature + * extraction plugins. For these, we also record the source model and + * the name of the transform used to generate the model so that we can + * regenerate it (potentially from a different source) on demand. + * + * -- A flat list of layers. Elsewhere, the GUI may distribute these + * across any number of view widgets. A layer may be viewable on more + * than one view at once, in principle. A layer refers to one model, + * but the same model can be in use in more than one layer. + * + * The document does *not* manage the existence or structure of Pane + * and other view widgets. However, it does provide convenience + * methods for reference-counted command-based management of the + * association between layers and views (addLayerToView, + * removeLayerFromView). + */ + +class Document : public QObject, + public XmlExportable +{ + Q_OBJECT + +public: + Document(); + virtual ~Document(); + + /** + * Create and return a new layer of the given type, associated + * with no model. The caller may set any model on this layer, but + * the model must also be registered with the document via the + * add-model methods below. + */ + Layer *createLayer(LayerFactory::LayerType); + + /** + * Create and return a new layer of the given type, associated + * with the current main model (if appropriate to the layer type). + */ + Layer *createMainModelLayer(LayerFactory::LayerType); + + /** + * Create and return a new layer associated with the given model, + * and register the model as an imported model. + */ + Layer *createImportedLayer(Model *); + + /** + * Create and return a new layer of the given type, with an + * appropriate empty model. If the given type is not one for + * which an empty model can meaningfully be created, return 0. + */ + Layer *createEmptyLayer(LayerFactory::LayerType); + + /** + * Create and return a new layer of the given type, associated + * with the given transform name. This method does not run the + * transform itself, nor create a model. The caller can safely + * add a model to the layer later, but note that all models used + * by a transform layer _must_ be registered with the document + * using addDerivedModel below. + */ + Layer *createDerivedLayer(LayerFactory::LayerType, TransformName); + + /** + * Create and return a suitable layer for the given transform, + * running the transform and associating the resulting model with + * the new layer. + */ + Layer *createDerivedLayer(TransformName, + Model *inputModel, + int inputChannel, // -1 -> all + QString configurationXml); + + /** + * Set the main model (the source for playback sample rate, etc) + * to the given wave file model. This will regenerate any derived + * models that were based on the previous main model. + */ + void setMainModel(WaveFileModel *); + + /** + * Get the main model (the source for playback sample rate, etc). + */ + WaveFileModel *getMainModel() { return m_mainModel; } + + /** + * Add a derived model associated with the given transform name. + * This is necessary to register any derived model that was not + * created by the document using + * e.g. createDerivedLayer(TransformName) above. + */ + void addDerivedModel(TransformName, + Model *inputModel, + int inputChannel, // -1 -> all + Model *outputModelToAdd, + QString configurationXml); + + /** + * Add an imported (non-derived, non-main) model. This is + * necessary to register any imported model that is associated + * with a layer. + */ + void addImportedModel(Model *); + + /** + * Associate the given model with the given layer. The model must + * have already been registered using one of the addXXModel + * methods above. + */ + void setModel(Layer *, Model *); + + /** + * Set the given layer to use the given channel of its model (-1 + * means all available channels). + */ + void setChannel(Layer *, int); + + /** + * Add the given layer to the given view. If the layer is + * intended to show a particular model, the model should normally + * be set using setModel before this method is called. + */ + void addLayerToView(View *, Layer *); + + /** + * Remove the given layer from the given view. + */ + void removeLayerFromView(View *, Layer *); + + void toXml(QTextStream &, QString indent, QString extraAttributes) const; + QString toXmlString(QString indent, QString extraAttributes) const; + +signals: + void layerAdded(Layer *); + void layerRemoved(Layer *); + void layerAboutToBeDeleted(Layer *); + + // Emitted when a layer is first added to a view, or when it is + // last removed from a view + void layerInAView(Layer *, bool); + + void modelAdded(Model *); + void mainModelChanged(WaveFileModel *); // emitted after modelAdded + void modelAboutToBeDeleted(Model *); + + void modelGenerationFailed(QString transformName); + void modelRegenerationFailed(QString layerName, QString transformName); + +protected: + Model *createModelForTransform(TransformName transform, + Model *inputModel, + int channel, + QString configurationXml); + void releaseModel(Model *model); + + /** + * Delete the given layer, and also its associated model if no + * longer used by any other layer. In general, this should be the + * only method used to delete layers -- doing so directly is a bit + * of a social gaffe. + */ + void deleteLayer(Layer *, bool force = false); + + /* + * Every model that is in use by a layer in the document must be + * found in either m_mainModel, m_derivedModels or + * m_importedModels. We own and control the lifespan of all of + * these models. + */ + + /** + * The model that provides the underlying sample rate, etc. This + * model is not reference counted for layers, and is not freed + * unless it is replaced or the document is deleted. + */ + WaveFileModel *m_mainModel; + + struct ModelRecord + { + // Information associated with a non-main model. If this + // model is derived from another, then source will be non-NULL + // and the transform name will be set appropriately. If the + // transform name is set but source is NULL, then there was a + // transform involved but the (target) model has been modified + // since being generated from it. + const Model *source; + TransformName transform; + int channel; + QString configurationXml; + + // Count of the number of layers using this model. + int refcount; + }; + + typedef std::map ModelMap; + ModelMap m_models; + + class AddLayerCommand : public Command + { + public: + AddLayerCommand(Document *d, View *view, Layer *layer); + virtual ~AddLayerCommand(); + + virtual void execute(); + virtual void unexecute(); + virtual QString getName() const { return m_name; } + + protected: + Document *m_d; + View *m_view; // I don't own this + Layer *m_layer; // Document owns this, but I determine its lifespans + QString m_name; + bool m_added; + }; + + class RemoveLayerCommand : public Command + { + public: + RemoveLayerCommand(Document *d, View *view, Layer *layer); + virtual ~RemoveLayerCommand(); + + virtual void execute(); + virtual void unexecute(); + virtual QString getName() const { return m_name; } + + protected: + Document *m_d; + View *m_view; // I don't own this + Layer *m_layer; // Document owns this, but I determine its lifespan + QString m_name; + bool m_added; + }; + + typedef std::map > LayerViewMap; + LayerViewMap m_layerViewMap; + + void addToLayerViewMap(Layer *, View *); + void removeFromLayerViewMap(Layer *, View *); + + QString getUniqueLayerName(QString candidate); + + /** + * And these are the layers. We also control the lifespans of + * these (usually through the commands used to add and remove them). + */ + typedef std::set LayerSet; + LayerSet m_layers; +}; + +#endif diff -r 000000000000 -r cd5d7ff8ef38 document/SVFileReader.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/document/SVFileReader.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,1005 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "SVFileReader.h" + +#include "base/Layer.h" +#include "base/View.h" +#include "base/PlayParameters.h" +#include "base/PlayParameterRepository.h" + +#include "AudioFileReaderFactory.h" + +#include "model/WaveFileModel.h" +#include "model/DenseThreeDimensionalModel.h" +#include "model/SparseOneDimensionalModel.h" +#include "model/SparseTimeValueModel.h" +#include "model/NoteModel.h" +#include "model/TextModel.h" + +#include "widgets/Pane.h" + +#include "main/Document.h" + +#include +#include +#include + +#include + +SVFileReader::SVFileReader(Document *document, + SVFileReaderPaneCallback &callback) : + m_document(document), + m_paneCallback(callback), + m_currentPane(0), + m_currentDataset(0), + m_currentDerivedModel(0), + m_currentPlayParameters(0), + m_datasetSeparator(" "), + m_inRow(false), + m_rowNumber(0), + m_ok(false) +{ +} + +void +SVFileReader::parse(const QString &xmlData) +{ + QXmlInputSource inputSource; + inputSource.setData(xmlData); + parse(inputSource); +} + +void +SVFileReader::parse(QXmlInputSource &inputSource) +{ + QXmlSimpleReader reader; + reader.setContentHandler(this); + reader.setErrorHandler(this); + m_ok = reader.parse(inputSource); +} + +bool +SVFileReader::isOK() +{ + return m_ok; +} + +SVFileReader::~SVFileReader() +{ + if (!m_awaitingDatasets.empty()) { + std::cerr << "WARNING: SV-XML: File ended with " + << m_awaitingDatasets.size() << " unfilled model dataset(s)" + << std::endl; + } + + std::set unaddedModels; + + for (std::map::iterator i = m_models.begin(); + i != m_models.end(); ++i) { + if (m_addedModels.find(i->second) == m_addedModels.end()) { + unaddedModels.insert(i->second); + } + } + + if (!unaddedModels.empty()) { + std::cerr << "WARNING: SV-XML: File contained " + << unaddedModels.size() << " unused models" + << std::endl; + while (!unaddedModels.empty()) { + delete *unaddedModels.begin(); + unaddedModels.erase(unaddedModels.begin()); + } + } +} + +bool +SVFileReader::startElement(const QString &, const QString &, + const QString &qName, + const QXmlAttributes &attributes) +{ + QString name = qName.toLower(); + + bool ok = false; + + // Valid element names: + // + // sv + // data + // dataset + // display + // derivation + // playparameters + // layer + // model + // point + // row + // view + // window + + if (name == "sv") { + + // nothing needed + ok = true; + + } else if (name == "data") { + + // nothing needed + m_inData = true; + ok = true; + + } else if (name == "display") { + + // nothing needed + ok = true; + + } else if (name == "window") { + + ok = readWindow(attributes); + + } else if (name == "model") { + + ok = readModel(attributes); + + } else if (name == "dataset") { + + ok = readDatasetStart(attributes); + + } else if (name == "bin") { + + ok = addBinToDataset(attributes); + + } else if (name == "point") { + + ok = addPointToDataset(attributes); + + } else if (name == "row") { + + ok = addRowToDataset(attributes); + + } else if (name == "layer") { + + addUnaddedModels(); // all models must be specified before first layer + ok = readLayer(attributes); + + } else if (name == "view") { + + m_inView = true; + ok = readView(attributes); + + } else if (name == "derivation") { + + ok = readDerivation(attributes); + + } else if (name == "playparameters") { + + ok = readPlayParameters(attributes); + + } else if (name == "plugin") { + + ok = readPlugin(attributes); + + } else if (name == "selections") { + + m_inSelections = true; + ok = true; + + } else if (name == "selection") { + + ok = readSelection(attributes); + } + + if (!ok) { + std::cerr << "WARNING: SV-XML: Failed to completely process element \"" + << name.toLocal8Bit().data() << "\"" << std::endl; + } + + return true; +} + +bool +SVFileReader::characters(const QString &text) +{ + bool ok = false; + + if (m_inRow) { + ok = readRowData(text); + if (!ok) { + std::cerr << "WARNING: SV-XML: Failed to read row data content for row " << m_rowNumber << std::endl; + } + } + + return true; +} + +bool +SVFileReader::endElement(const QString &, const QString &, + const QString &qName) +{ + QString name = qName.toLower(); + + if (name == "dataset") { + + if (m_currentDataset) { + + bool foundInAwaiting = false; + + for (std::map::iterator i = m_awaitingDatasets.begin(); + i != m_awaitingDatasets.end(); ++i) { + if (m_models[i->second] == m_currentDataset) { + m_awaitingDatasets.erase(i); + foundInAwaiting = true; + break; + } + } + + if (!foundInAwaiting) { + std::cerr << "WARNING: SV-XML: Dataset precedes model, or no model uses dataset" << std::endl; + } + } + + m_currentDataset = 0; + + } else if (name == "data") { + + addUnaddedModels(); + m_inData = false; + + } else if (name == "derivation") { + + if (m_currentDerivedModel) { + m_document->addDerivedModel(m_currentTransform, + m_document->getMainModel(), //!!! + m_currentTransformChannel, + m_currentDerivedModel, + m_currentTransformConfiguration); + m_addedModels.insert(m_currentDerivedModel); + m_currentDerivedModel = 0; + m_currentTransform = ""; + m_currentTransformConfiguration = ""; + } + + } else if (name == "row") { + m_inRow = false; + } else if (name == "view") { + m_inView = false; + } else if (name == "selections") { + m_inSelections = false; + } else if (name == "playparameters") { + m_currentPlayParameters = 0; + } + + return true; +} + +bool +SVFileReader::error(const QXmlParseException &exception) +{ + m_errorString = + QString("ERROR: SV-XML: %1 at line %2, column %3") + .arg(exception.message()) + .arg(exception.lineNumber()) + .arg(exception.columnNumber()); + std::cerr << m_errorString.toLocal8Bit().data() << std::endl; + return QXmlDefaultHandler::error(exception); +} + +bool +SVFileReader::fatalError(const QXmlParseException &exception) +{ + m_errorString = + QString("FATAL ERROR: SV-XML: %1 at line %2, column %3") + .arg(exception.message()) + .arg(exception.lineNumber()) + .arg(exception.columnNumber()); + std::cerr << m_errorString.toLocal8Bit().data() << std::endl; + return QXmlDefaultHandler::fatalError(exception); +} + + +#define READ_MANDATORY(TYPE, NAME, CONVERSION) \ + TYPE NAME = attributes.value(#NAME).trimmed().CONVERSION(&ok); \ + if (!ok) { \ + std::cerr << "WARNING: SV-XML: Missing or invalid mandatory " #TYPE " attribute \"" #NAME "\"" << std::endl; \ + return false; \ + } + +bool +SVFileReader::readWindow(const QXmlAttributes &attributes) +{ + bool ok = false; + + READ_MANDATORY(int, width, toInt); + READ_MANDATORY(int, height, toInt); + + m_paneCallback.setWindowSize(width, height); + return true; +} + +void +SVFileReader::addUnaddedModels() +{ + std::set unaddedModels; + + for (std::map::iterator i = m_models.begin(); + i != m_models.end(); ++i) { + if (m_addedModels.find(i->second) == m_addedModels.end()) { + unaddedModels.insert(i->second); + } + } + + for (std::set::iterator i = unaddedModels.begin(); + i != unaddedModels.end(); ++i) { + m_document->addImportedModel(*i); + m_addedModels.insert(*i); + } +} + +bool +SVFileReader::readModel(const QXmlAttributes &attributes) +{ + bool ok = false; + + READ_MANDATORY(int, id, toInt); + + if (m_models.find(id) != m_models.end()) { + std::cerr << "WARNING: SV-XML: Ignoring duplicate model id " << id + << std::endl; + return false; + } + + QString name = attributes.value("name"); + + READ_MANDATORY(int, sampleRate, toInt); + + QString type = attributes.value("type").trimmed(); + bool mainModel = (attributes.value("mainModel").trimmed() == "true"); + + if (type == "wavefile") { + + QString file = attributes.value("file"); + WaveFileModel *model = new WaveFileModel(file); + + while (!model->isOK()) { + + delete model; + model = 0; + + if (QMessageBox::question(0, + QMessageBox::tr("Failed to open file"), + QMessageBox::tr("Audio file \"%1\" could not be opened.\nLocate it?").arg(file), + QMessageBox::Ok, + QMessageBox::Cancel) == QMessageBox::Ok) { + + QString path = QFileDialog::getOpenFileName + (0, QFileDialog::tr("Locate file \"%1\"").arg(QFileInfo(file).fileName()), file, + QFileDialog::tr("Audio files (%1)\nAll files (*.*)") + .arg(AudioFileReaderFactory::getKnownExtensions())); + + if (path != "") { + model = new WaveFileModel(path); + } else { + return false; + } + } else { + return false; + } + } + + m_models[id] = model; + if (mainModel) { + m_document->setMainModel(model); + m_addedModels.insert(model); + } + // Derived models will be added when their derivation + // is found. + + return true; + + } else if (type == "dense") { + + READ_MANDATORY(int, dimensions, toInt); + + // Currently the only dense model we support here + // is the dense 3d model. Dense time-value models + // are always file-backed waveform data, at this + // point, and they come in as the wavefile model + // type above. + + if (dimensions == 3) { + + READ_MANDATORY(int, windowSize, toInt); + READ_MANDATORY(int, yBinCount, toInt); + + DenseThreeDimensionalModel *model = + new DenseThreeDimensionalModel(sampleRate, windowSize, yBinCount); + + float minimum = attributes.value("minimum").trimmed().toFloat(&ok); + if (ok) model->setMinimumLevel(minimum); + + float maximum = attributes.value("maximum").trimmed().toFloat(&ok); + if (ok) model->setMaximumLevel(maximum); + + int dataset = attributes.value("dataset").trimmed().toInt(&ok); + if (ok) m_awaitingDatasets[dataset] = id; + + m_models[id] = model; + return true; + + } else { + + std::cerr << "WARNING: SV-XML: Unexpected dense model dimension (" + << dimensions << ")" << std::endl; + } + } else if (type == "sparse") { + + READ_MANDATORY(int, dimensions, toInt); + + if (dimensions == 1) { + + READ_MANDATORY(int, resolution, toInt); + + SparseOneDimensionalModel *model = new SparseOneDimensionalModel + (sampleRate, resolution); + m_models[id] = model; + + int dataset = attributes.value("dataset").trimmed().toInt(&ok); + if (ok) m_awaitingDatasets[dataset] = id; + + return true; + + } else if (dimensions == 2 || dimensions == 3) { + + READ_MANDATORY(int, resolution, toInt); + + float minimum = attributes.value("minimum").trimmed().toFloat(&ok); + float maximum = attributes.value("maximum").trimmed().toFloat(&ok); + float valueQuantization = + attributes.value("valueQuantization").trimmed().toFloat(&ok); + + bool notifyOnAdd = (attributes.value("notifyOnAdd") == "true"); + + QString units = attributes.value("units"); + + if (dimensions == 2) { + if (attributes.value("subtype") == "text") { + TextModel *model = new TextModel + (sampleRate, resolution, notifyOnAdd); + m_models[id] = model; + } else { + SparseTimeValueModel *model = new SparseTimeValueModel + (sampleRate, resolution, minimum, maximum, notifyOnAdd); + model->setScaleUnits(units); + m_models[id] = model; + } + } else { + NoteModel *model = new NoteModel + (sampleRate, resolution, minimum, maximum, notifyOnAdd); + model->setValueQuantization(valueQuantization); + model->setScaleUnits(units); + m_models[id] = model; + } + + int dataset = attributes.value("dataset").trimmed().toInt(&ok); + if (ok) m_awaitingDatasets[dataset] = id; + + return true; + + } else { + + std::cerr << "WARNING: SV-XML: Unexpected sparse model dimension (" + << dimensions << ")" << std::endl; + } + } else { + + std::cerr << "WARNING: SV-XML: Unexpected model type \"" + << type.toLocal8Bit().data() << "\" for model id" << id << std::endl; + } + + return false; +} + +bool +SVFileReader::readView(const QXmlAttributes &attributes) +{ + QString type = attributes.value("type"); + m_currentPane = 0; + + if (type != "pane") { + std::cerr << "WARNING: SV-XML: Unexpected view type \"" + << type.toLocal8Bit().data() << "\"" << std::endl; + return false; + } + + m_currentPane = m_paneCallback.addPane(); + + if (!m_currentPane) { + std::cerr << "WARNING: SV-XML: Internal error: Failed to add pane!" + << std::endl; + return false; + } + + bool ok = false; + + View *view = m_currentPane; + + // The view properties first + + READ_MANDATORY(size_t, centre, toUInt); + READ_MANDATORY(size_t, zoom, toUInt); + READ_MANDATORY(int, followPan, toInt); + READ_MANDATORY(int, followZoom, toInt); + READ_MANDATORY(int, light, toInt); + QString tracking = attributes.value("tracking"); + + // Specify the follow modes before we set the actual values + view->setFollowGlobalPan(followPan); + view->setFollowGlobalZoom(followZoom); + view->setPlaybackFollow(tracking == "scroll" ? View::PlaybackScrollContinuous : + tracking == "page" ? View::PlaybackScrollPage + : View::PlaybackIgnore); + + // Then set these values + view->setCentreFrame(centre); + view->setZoomLevel(zoom); + view->setLightBackground(light); + + // And pane properties + READ_MANDATORY(int, centreLineVisible, toInt); + m_currentPane->setCentreLineVisible(centreLineVisible); + + int height = attributes.value("height").toInt(&ok); + if (ok) { + m_currentPane->resize(m_currentPane->width(), height); + } + + return true; +} + +bool +SVFileReader::readLayer(const QXmlAttributes &attributes) +{ + QString type = attributes.value("type"); + + int id; + bool ok = false; + id = attributes.value("id").trimmed().toInt(&ok); + + if (!ok) { + std::cerr << "WARNING: SV-XML: No layer id for layer of type \"" + << type.toLocal8Bit().data() + << "\"" << std::endl; + return false; + } + + Layer *layer = 0; + bool isNewLayer = false; + + // Layers are expected to be defined in layer elements in the data + // section, and referred to in layer elements in the view + // sections. So if we're in the data section, we expect this + // layer not to exist already; if we're in the view section, we + // expect it to exist. + + if (m_inData) { + + if (m_layers.find(id) != m_layers.end()) { + std::cerr << "WARNING: SV-XML: Ignoring duplicate layer id " << id + << " in data section" << std::endl; + return false; + } + + layer = m_layers[id] = m_document->createLayer + (LayerFactory::getInstance()->getLayerTypeForName(type)); + + if (layer) { + m_layers[id] = layer; + isNewLayer = true; + } + + } else { + + if (!m_currentPane) { + std::cerr << "WARNING: SV-XML: No current pane for layer " << id + << " in view section" << std::endl; + return false; + } + + if (m_layers.find(id) != m_layers.end()) { + + layer = m_layers[id]; + + } else { + std::cerr << "WARNING: SV-XML: Layer id " << id + << " in view section has not been defined -- defining it here" + << std::endl; + + layer = m_document->createLayer + (LayerFactory::getInstance()->getLayerTypeForName(type)); + + if (layer) { + m_layers[id] = layer; + isNewLayer = true; + } + } + } + + if (!layer) { + std::cerr << "WARNING: SV-XML: Failed to add layer of type \"" + << type.toLocal8Bit().data() + << "\"" << std::endl; + return false; + } + + if (isNewLayer) { + + QString name = attributes.value("name"); + layer->setObjectName(name); + + int modelId; + bool modelOk = false; + modelId = attributes.value("model").trimmed().toInt(&modelOk); + + if (modelOk) { + if (m_models.find(modelId) != m_models.end()) { + Model *model = m_models[modelId]; + m_document->setModel(layer, model); + } else { + std::cerr << "WARNING: SV-XML: Unknown model id " << modelId + << " in layer definition" << std::endl; + } + } + + layer->setProperties(attributes); + } + + if (!m_inData && m_currentPane) { + m_document->addLayerToView(m_currentPane, layer); + } + + return true; +} + +bool +SVFileReader::readDatasetStart(const QXmlAttributes &attributes) +{ + bool ok = false; + + READ_MANDATORY(int, id, toInt); + READ_MANDATORY(int, dimensions, toInt); + + if (m_awaitingDatasets.find(id) == m_awaitingDatasets.end()) { + std::cerr << "WARNING: SV-XML: Unwanted dataset " << id << std::endl; + return false; + } + + int modelId = m_awaitingDatasets[id]; + + Model *model = 0; + if (m_models.find(modelId) != m_models.end()) { + model = m_models[modelId]; + } else { + std::cerr << "WARNING: SV-XML: Internal error: Unknown model " << modelId + << " expecting dataset " << id << std::endl; + return false; + } + + bool good = false; + + switch (dimensions) { + case 1: + if (dynamic_cast(model)) good = true; + break; + + case 2: + if (dynamic_cast(model)) good = true; + else if (dynamic_cast(model)) good = true; + break; + + case 3: + if (dynamic_cast(model)) good = true; + else if (dynamic_cast(model)) { + m_datasetSeparator = attributes.value("separator"); + good = true; + } + break; + } + + if (!good) { + std::cerr << "WARNING: SV-XML: Model id " << modelId << " has wrong number of dimensions for " << dimensions << "-D dataset " << id << std::endl; + m_currentDataset = 0; + return false; + } + + m_currentDataset = model; + return true; +} + +bool +SVFileReader::addPointToDataset(const QXmlAttributes &attributes) +{ + bool ok = false; + + READ_MANDATORY(int, frame, toInt); + + SparseOneDimensionalModel *sodm = dynamic_cast + (m_currentDataset); + + if (sodm) { + QString label = attributes.value("label"); + sodm->addPoint(SparseOneDimensionalModel::Point(frame, label)); + return true; + } + + SparseTimeValueModel *stvm = dynamic_cast + (m_currentDataset); + + if (stvm) { + float value = 0.0; + value = attributes.value("value").trimmed().toFloat(&ok); + QString label = attributes.value("label"); + stvm->addPoint(SparseTimeValueModel::Point(frame, value, label)); + return ok; + } + + NoteModel *nm = dynamic_cast(m_currentDataset); + + if (nm) { + float value = 0.0; + value = attributes.value("value").trimmed().toFloat(&ok); + float duration = 0.0; + duration = attributes.value("duration").trimmed().toFloat(&ok); + QString label = attributes.value("label"); + nm->addPoint(NoteModel::Point(frame, value, duration, label)); + return ok; + } + + TextModel *tm = dynamic_cast(m_currentDataset); + + if (tm) { + float height = 0.0; + height = attributes.value("height").trimmed().toFloat(&ok); + QString label = attributes.value("label"); + tm->addPoint(TextModel::Point(frame, height, label)); + return ok; + } + + std::cerr << "WARNING: SV-XML: Point element found in non-point dataset" << std::endl; + + return false; +} + +bool +SVFileReader::addBinToDataset(const QXmlAttributes &attributes) +{ + DenseThreeDimensionalModel *dtdm = dynamic_cast + (m_currentDataset); + + if (dtdm) { + + bool ok = false; + int n = attributes.value("number").trimmed().toInt(&ok); + if (!ok) { + std::cerr << "WARNING: SV-XML: Missing or invalid bin number" + << std::endl; + return false; + } + + QString name = attributes.value("name"); + + dtdm->setBinName(n, name); + return true; + } + + std::cerr << "WARNING: SV-XML: Bin definition found in incompatible dataset" << std::endl; + + return false; +} + + +bool +SVFileReader::addRowToDataset(const QXmlAttributes &attributes) +{ + m_inRow = false; + + bool ok = false; + m_rowNumber = attributes.value("n").trimmed().toInt(&ok); + if (!ok) { + std::cerr << "WARNING: SV-XML: Missing or invalid row number" + << std::endl; + return false; + } + + m_inRow = true; + +// std::cerr << "SV-XML: In row " << m_rowNumber << std::endl; + + return true; +} + +bool +SVFileReader::readRowData(const QString &text) +{ + DenseThreeDimensionalModel *dtdm = dynamic_cast + (m_currentDataset); + + bool warned = false; + + if (dtdm) { + QStringList data = text.split(m_datasetSeparator); + + DenseThreeDimensionalModel::BinValueSet values; + + for (QStringList::iterator i = data.begin(); i != data.end(); ++i) { + + if (values.size() == dtdm->getYBinCount()) { + if (!warned) { + std::cerr << "WARNING: SV-XML: Too many y-bins in 3-D dataset row " + << m_rowNumber << std::endl; + warned = true; + } + } + + bool ok; + float value = i->toFloat(&ok); + if (!ok) { + std::cerr << "WARNING: SV-XML: Bad floating-point value " + << i->toLocal8Bit().data() + << " in row data" << std::endl; + } else { + values.push_back(value); + } + } + + size_t windowStartFrame = m_rowNumber * dtdm->getWindowSize(); + + dtdm->setBinValues(windowStartFrame, values); + return true; + } + + std::cerr << "WARNING: SV-XML: Row data found in non-row dataset" << std::endl; + + return false; +} + +bool +SVFileReader::readDerivation(const QXmlAttributes &attributes) +{ + int modelId = 0; + bool modelOk = false; + modelId = attributes.value("model").trimmed().toInt(&modelOk); + + if (!modelOk) { + std::cerr << "WARNING: SV-XML: No model id specified for derivation" << std::endl; + return false; + } + + QString transform = attributes.value("transform"); + + if (m_models.find(modelId) != m_models.end()) { + + m_currentDerivedModel = m_models[modelId]; + m_currentTransform = transform; + m_currentTransformConfiguration = ""; + + bool ok = false; + int channel = attributes.value("channel").trimmed().toInt(&ok); + if (ok) m_currentTransformChannel = channel; + else m_currentTransformChannel = -1; + + } else { + std::cerr << "WARNING: SV-XML: Unknown derived model " << modelId + << " for transform \"" << transform.toLocal8Bit().data() << "\"" + << std::endl; + return false; + } + + return true; +} + +bool +SVFileReader::readPlayParameters(const QXmlAttributes &attributes) +{ + m_currentPlayParameters = 0; + + int modelId = 0; + bool modelOk = false; + modelId = attributes.value("model").trimmed().toInt(&modelOk); + + if (!modelOk) { + std::cerr << "WARNING: SV-XML: No model id specified for play parameters" << std::endl; + return false; + } + + if (m_models.find(modelId) != m_models.end()) { + + bool ok = false; + + PlayParameters *parameters = PlayParameterRepository::getInstance()-> + getPlayParameters(m_models[modelId]); + + if (!parameters) { + std::cerr << "WARNING: SV-XML: Play parameters for model " + << modelId + << " not found - has model been added to document?" + << std::endl; + return false; + } + + bool muted = (attributes.value("mute").trimmed() == "true"); + if (ok) parameters->setPlayMuted(muted); + + float pan = attributes.value("pan").toFloat(&ok); + if (ok) parameters->setPlayPan(pan); + + float gain = attributes.value("gain").toFloat(&ok); + if (ok) parameters->setPlayGain(gain); + + QString pluginId = attributes.value("pluginId"); + if (pluginId != "") parameters->setPlayPluginId(pluginId); + + m_currentPlayParameters = parameters; + +// std::cerr << "Current play parameters for model: " << m_models[modelId] << ": " << m_currentPlayParameters << std::endl; + + } else { + + std::cerr << "WARNING: SV-XML: Unknown model " << modelId + << " for play parameters" << std::endl; + return false; + } + + return true; +} + +bool +SVFileReader::readPlugin(const QXmlAttributes &attributes) +{ + if (!m_currentDerivedModel && !m_currentPlayParameters) { + std::cerr << "WARNING: SV-XML: Plugin found outside derivation or play parameters" << std::endl; + return false; + } + + QString configurationXml = "setPlayPluginConfiguration(configurationXml); + } else { + m_currentTransformConfiguration += configurationXml; + } + + return true; +} + +bool +SVFileReader::readSelection(const QXmlAttributes &attributes) +{ + bool ok; + + READ_MANDATORY(int, start, toInt); + READ_MANDATORY(int, end, toInt); + + m_paneCallback.addSelection(start, end); + + return true; +} + diff -r 000000000000 -r cd5d7ff8ef38 document/SVFileReader.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/document/SVFileReader.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,108 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _SV_FILE_READER_H_ +#define _SV_FILE_READER_H_ + +#include "layer/LayerFactory.h" +#include "transform/Transform.h" + +#include + +#include + +class Pane; +class Model; +class Document; +class PlayParameters; + +class SVFileReaderPaneCallback +{ +public: + virtual Pane *addPane() = 0; + virtual void setWindowSize(int width, int height) = 0; + virtual void addSelection(int start, int end) = 0; +}; + +class SVFileReader : public QXmlDefaultHandler +{ +public: + SVFileReader(Document *document, + SVFileReaderPaneCallback &callback); + virtual ~SVFileReader(); + + void parse(const QString &xmlData); + void parse(QXmlInputSource &source); + + bool isOK(); + QString getErrorString() const { return m_errorString; } + + // For loading a single layer onto an existing pane + void setCurrentPane(Pane *pane) { m_currentPane = pane; } + + virtual bool startElement(const QString &namespaceURI, + const QString &localName, + const QString &qName, + const QXmlAttributes& atts); + + virtual bool characters(const QString &); + + virtual bool endElement(const QString &namespaceURI, + const QString &localName, + const QString &qName); + + bool error(const QXmlParseException &exception); + bool fatalError(const QXmlParseException &exception); + +protected: + bool readWindow(const QXmlAttributes &); + bool readModel(const QXmlAttributes &); + bool readView(const QXmlAttributes &); + bool readLayer(const QXmlAttributes &); + bool readDatasetStart(const QXmlAttributes &); + bool addBinToDataset(const QXmlAttributes &); + bool addPointToDataset(const QXmlAttributes &); + bool addRowToDataset(const QXmlAttributes &); + bool readRowData(const QString &); + bool readDerivation(const QXmlAttributes &); + bool readPlayParameters(const QXmlAttributes &); + bool readPlugin(const QXmlAttributes &); + bool readSelection(const QXmlAttributes &); + void addUnaddedModels(); + + Document *m_document; + SVFileReaderPaneCallback &m_paneCallback; + Pane *m_currentPane; + std::map m_layers; + std::map m_models; + std::set m_addedModels; + std::map m_awaitingDatasets; // map dataset id -> model id + Model *m_currentDataset; + Model *m_currentDerivedModel; + PlayParameters *m_currentPlayParameters; + QString m_currentTransform; + int m_currentTransformChannel; + QString m_currentTransformConfiguration; + QString m_datasetSeparator; + bool m_inRow; + bool m_inView; + bool m_inData; + bool m_inSelections; + int m_rowNumber; + QString m_errorString; + bool m_ok; +}; + +#endif diff -r 000000000000 -r cd5d7ff8ef38 main/MainWindow.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main/MainWindow.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,3097 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "../version.h" + +#include "MainWindow.h" +#include "Document.h" +#include "PreferencesDialog.h" + +#include "widgets/Pane.h" +#include "widgets/PaneStack.h" +#include "model/WaveFileModel.h" +#include "model/SparseOneDimensionalModel.h" +#include "base/ViewManager.h" +#include "base/Preferences.h" +#include "layer/WaveformLayer.h" +#include "layer/TimeRulerLayer.h" +#include "layer/TimeInstantLayer.h" +#include "layer/TimeValueLayer.h" +#include "layer/Colour3DPlotLayer.h" +#include "widgets/Fader.h" +#include "widgets/Panner.h" +#include "widgets/PropertyBox.h" +#include "widgets/PropertyStack.h" +#include "widgets/AudioDial.h" +#include "widgets/LayerTree.h" +#include "widgets/ListInputDialog.h" +#include "audioio/AudioCallbackPlaySource.h" +#include "audioio/AudioCallbackPlayTarget.h" +#include "audioio/AudioTargetFactory.h" +#include "fileio/AudioFileReaderFactory.h" +#include "fileio/DataFileReaderFactory.h" +#include "fileio/WavFileWriter.h" +#include "fileio/CSVFileWriter.h" +#include "fileio/BZipFileDevice.h" +#include "fileio/RecentFiles.h" +#include "transform/TransformFactory.h" +#include "base/PlayParameterRepository.h" +#include "base/XmlExportable.h" +#include "base/CommandHistory.h" +#include "base/Profiler.h" +#include "base/Clipboard.h" + +// For version information +#include "vamp/vamp.h" +#include "vamp-sdk/PluginBase.h" +#include "plugin/api/ladspa.h" +#include "plugin/api/dssi.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using std::cerr; +using std::endl; + + +MainWindow::MainWindow() : + m_document(0), + m_paneStack(0), + m_viewManager(0), + m_panner(0), + m_timeRulerLayer(0), + m_playSource(0), + m_playTarget(0), + m_mainMenusCreated(false), + m_paneMenu(0), + m_layerMenu(0), + m_existingLayersMenu(0), + m_rightButtonMenu(0), + m_rightButtonLayerMenu(0), + m_documentModified(false), + m_preferencesDialog(0) +{ + setWindowTitle(tr("Sonic Visualiser")); + + UnitDatabase::getInstance()->registerUnit("Hz"); + UnitDatabase::getInstance()->registerUnit("dB"); + + connect(CommandHistory::getInstance(), SIGNAL(commandExecuted()), + this, SLOT(documentModified())); + connect(CommandHistory::getInstance(), SIGNAL(documentRestored()), + this, SLOT(documentRestored())); + + QFrame *frame = new QFrame; + setCentralWidget(frame); + + QGridLayout *layout = new QGridLayout; + + m_viewManager = new ViewManager(); + connect(m_viewManager, SIGNAL(selectionChanged()), + this, SLOT(updateMenuStates())); + + m_descriptionLabel = new QLabel; + + m_paneStack = new PaneStack(frame, m_viewManager); + connect(m_paneStack, SIGNAL(currentPaneChanged(Pane *)), + this, SLOT(currentPaneChanged(Pane *))); + connect(m_paneStack, SIGNAL(currentLayerChanged(Pane *, Layer *)), + this, SLOT(currentLayerChanged(Pane *, Layer *))); + connect(m_paneStack, SIGNAL(rightButtonMenuRequested(Pane *, QPoint)), + this, SLOT(rightButtonMenuRequested(Pane *, QPoint))); + + m_panner = new Panner(frame); + m_panner->setViewManager(m_viewManager); + m_panner->setFixedHeight(40); + + m_panLayer = new WaveformLayer; + m_panLayer->setChannelMode(WaveformLayer::MergeChannels); +// m_panLayer->setScale(WaveformLayer::MeterScale); + m_panLayer->setAutoNormalize(true); + m_panLayer->setBaseColour(Qt::darkGreen); + m_panLayer->setAggressiveCacheing(true); + m_panner->addLayer(m_panLayer); + + m_playSource = new AudioCallbackPlaySource(m_viewManager); + + connect(m_playSource, SIGNAL(sampleRateMismatch(size_t, size_t, bool)), + this, SLOT(sampleRateMismatch(size_t, size_t, bool))); + + m_fader = new Fader(frame, false); + + m_playSpeed = new AudioDial(frame); + m_playSpeed->setMinimum(1); + m_playSpeed->setMaximum(10); + m_playSpeed->setValue(10); + m_playSpeed->setFixedWidth(24); + m_playSpeed->setFixedHeight(24); + m_playSpeed->setNotchesVisible(true); + m_playSpeed->setPageStep(1); + m_playSpeed->setToolTip(tr("Playback speed: Full")); + m_playSpeed->setDefaultValue(10); + connect(m_playSpeed, SIGNAL(valueChanged(int)), + this, SLOT(playSpeedChanged(int))); + + layout->addWidget(m_paneStack, 0, 0, 1, 3); + layout->addWidget(m_panner, 1, 0); + layout->addWidget(m_fader, 1, 1); + layout->addWidget(m_playSpeed, 1, 2); + frame->setLayout(layout); + + connect(m_viewManager, SIGNAL(outputLevelsChanged(float, float)), + this, SLOT(outputLevelsChanged(float, float))); + + connect(Preferences::getInstance(), + SIGNAL(propertyChanged(PropertyContainer::PropertyName)), + this, + SLOT(preferenceChanged(PropertyContainer::PropertyName))); + + setupMenus(); + setupToolbars(); + +// statusBar()->addWidget(m_descriptionLabel); + + newSession(); +} + +MainWindow::~MainWindow() +{ + closeSession(); + delete m_playTarget; + delete m_playSource; + delete m_viewManager; + Profiles::getInstance()->dump(); +} + +void +MainWindow::setupMenus() +{ + QAction *action = 0; + QMenu *menu = 0; + QToolBar *toolbar = 0; + + if (!m_mainMenusCreated) { + m_rightButtonMenu = new QMenu(); + } + + if (m_rightButtonLayerMenu) { + m_rightButtonLayerMenu->clear(); + } else { + m_rightButtonLayerMenu = m_rightButtonMenu->addMenu(tr("&Layer")); + m_rightButtonMenu->addSeparator(); + } + + if (!m_mainMenusCreated) { + + CommandHistory::getInstance()->registerMenu(m_rightButtonMenu); + m_rightButtonMenu->addSeparator(); + + menu = menuBar()->addMenu(tr("&File")); + toolbar = addToolBar(tr("File Toolbar")); + + QIcon icon(":icons/filenew.png"); + icon.addFile(":icons/filenew-22.png"); + action = new QAction(icon, tr("&New Session"), this); + action->setShortcut(tr("Ctrl+N")); + action->setStatusTip(tr("Clear the current Sonic Visualiser session and start a new one")); + connect(action, SIGNAL(triggered()), this, SLOT(newSession())); + menu->addAction(action); + toolbar->addAction(action); + + icon = QIcon(":icons/fileopen.png"); + icon.addFile(":icons/fileopen-22.png"); + + action = new QAction(icon, tr("&Open Session..."), this); + action->setShortcut(tr("Ctrl+O")); + action->setStatusTip(tr("Open a previously saved Sonic Visualiser session file")); + connect(action, SIGNAL(triggered()), this, SLOT(openSession())); + menu->addAction(action); + + action = new QAction(icon, tr("&Open..."), this); + action->setStatusTip(tr("Open a session file, audio file, or layer")); + connect(action, SIGNAL(triggered()), this, SLOT(openSomething())); + toolbar->addAction(action); + + icon = QIcon(":icons/filesave.png"); + icon.addFile(":icons/filesave-22.png"); + action = new QAction(icon, tr("&Save Session"), this); + action->setShortcut(tr("Ctrl+S")); + action->setStatusTip(tr("Save the current session into a Sonic Visualiser session file")); + connect(action, SIGNAL(triggered()), this, SLOT(saveSession())); + connect(this, SIGNAL(canSave(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + toolbar->addAction(action); + + icon = QIcon(":icons/filesaveas.png"); + icon.addFile(":icons/filesaveas-22.png"); + action = new QAction(icon, tr("Save Session &As..."), this); + action->setStatusTip(tr("Save the current session into a new Sonic Visualiser session file")); + connect(action, SIGNAL(triggered()), this, SLOT(saveSessionAs())); + menu->addAction(action); + toolbar->addAction(action); + + menu->addSeparator(); + + action = new QAction(tr("&Import Audio File..."), this); + action->setShortcut(tr("Ctrl+I")); + action->setStatusTip(tr("Import an existing audio file")); + connect(action, SIGNAL(triggered()), this, SLOT(importAudio())); + menu->addAction(action); + + action = new QAction(tr("Import Secondary Audio File..."), this); + action->setShortcut(tr("Ctrl+Shift+I")); + action->setStatusTip(tr("Import an extra audio file as a separate layer")); + connect(action, SIGNAL(triggered()), this, SLOT(importMoreAudio())); + connect(this, SIGNAL(canImportMoreAudio(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(tr("&Export Audio File..."), this); + action->setStatusTip(tr("Export selection as an audio file")); + connect(action, SIGNAL(triggered()), this, SLOT(exportAudio())); + connect(this, SIGNAL(canExportAudio(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + menu->addSeparator(); + + action = new QAction(tr("Import Annotation &Layer..."), this); + action->setShortcut(tr("Ctrl+L")); + action->setStatusTip(tr("Import layer data from an existing file")); + connect(action, SIGNAL(triggered()), this, SLOT(importLayer())); + connect(this, SIGNAL(canImportLayer(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(tr("Export Annotation Layer..."), this); + action->setStatusTip(tr("Export layer data to a file")); + connect(action, SIGNAL(triggered()), this, SLOT(exportLayer())); + connect(this, SIGNAL(canExportLayer(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + menu->addSeparator(); + m_recentFilesMenu = menu->addMenu(tr("&Recent Files")); + menu->addMenu(m_recentFilesMenu); + setupRecentFilesMenu(); + connect(RecentFiles::getInstance(), SIGNAL(recentFilesChanged()), + this, SLOT(setupRecentFilesMenu())); + + menu->addSeparator(); + action = new QAction(tr("&Preferences..."), this); + action->setStatusTip(tr("Adjust the application preferences")); + connect(action, SIGNAL(triggered()), this, SLOT(preferences())); + menu->addAction(action); + + /*!!! + menu->addSeparator(); + + action = new QAction(tr("Play / Pause"), this); + action->setShortcut(tr("Space")); + action->setStatusTip(tr("Start or stop playback from the current position")); + connect(action, SIGNAL(triggered()), this, SLOT(play())); + menu->addAction(action); + */ + + menu->addSeparator(); + action = new QAction(QIcon(":/icons/exit.png"), + tr("&Quit"), this); + action->setShortcut(tr("Ctrl+Q")); + connect(action, SIGNAL(triggered()), this, SLOT(close())); + menu->addAction(action); + + menu = menuBar()->addMenu(tr("&Edit")); + CommandHistory::getInstance()->registerMenu(menu); + + menu->addSeparator(); + + action = new QAction(QIcon(":/icons/editcut.png"), + tr("Cu&t"), this); + action->setShortcut(tr("Ctrl+X")); + connect(action, SIGNAL(triggered()), this, SLOT(cut())); + connect(this, SIGNAL(canEditSelection(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + action = new QAction(QIcon(":/icons/editcopy.png"), + tr("&Copy"), this); + action->setShortcut(tr("Ctrl+C")); + connect(action, SIGNAL(triggered()), this, SLOT(copy())); + connect(this, SIGNAL(canEditSelection(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + action = new QAction(QIcon(":/icons/editpaste.png"), + tr("&Paste"), this); + action->setShortcut(tr("Ctrl+V")); + connect(action, SIGNAL(triggered()), this, SLOT(paste())); + connect(this, SIGNAL(canPaste(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + action = new QAction(tr("&Delete Selected Items"), this); + action->setShortcut(tr("Del")); + connect(action, SIGNAL(triggered()), this, SLOT(deleteSelected())); + connect(this, SIGNAL(canEditSelection(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + menu->addSeparator(); + m_rightButtonMenu->addSeparator(); + + action = new QAction(tr("Select &All"), this); + action->setShortcut(tr("Ctrl+A")); + connect(action, SIGNAL(triggered()), this, SLOT(selectAll())); + connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + action = new QAction(tr("Select &Visible Range"), this); + action->setShortcut(tr("Ctrl+Shift+A")); + connect(action, SIGNAL(triggered()), this, SLOT(selectVisible())); + connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(tr("Select to &Start"), this); + action->setShortcut(tr("Shift+Left")); + connect(action, SIGNAL(triggered()), this, SLOT(selectToStart())); + connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(tr("Select to &End"), this); + action->setShortcut(tr("Shift+Right")); + connect(action, SIGNAL(triggered()), this, SLOT(selectToEnd())); + connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(tr("C&lear Selection"), this); + action->setShortcut(tr("Esc")); + connect(action, SIGNAL(triggered()), this, SLOT(clearSelection())); + connect(this, SIGNAL(canClearSelection(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + menu->addSeparator(); + + action = new QAction(tr("&Insert Instant at Playback Position"), this); + action->setShortcut(tr("Enter")); + connect(action, SIGNAL(triggered()), this, SLOT(insertInstant())); + connect(this, SIGNAL(canInsertInstant(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + menu = menuBar()->addMenu(tr("&View")); + + QActionGroup *overlayGroup = new QActionGroup(this); + + action = new QAction(tr("&No Text Overlays"), this); + action->setShortcut(tr("0")); + action->setStatusTip(tr("Show no texts for frame times, layer names etc")); + connect(action, SIGNAL(triggered()), this, SLOT(showNoOverlays())); + action->setCheckable(true); + action->setChecked(false); + overlayGroup->addAction(action); + menu->addAction(action); + + action = new QAction(tr("Basic &Text Overlays"), this); + action->setShortcut(tr("9")); + action->setStatusTip(tr("Show texts for frame times etc, but not layer names etc")); + connect(action, SIGNAL(triggered()), this, SLOT(showBasicOverlays())); + action->setCheckable(true); + action->setChecked(true); + overlayGroup->addAction(action); + menu->addAction(action); + + action = new QAction(tr("&All Text Overlays"), this); + action->setShortcut(tr("8")); + action->setStatusTip(tr("Show texts for frame times, layer names etc")); + connect(action, SIGNAL(triggered()), this, SLOT(showAllOverlays())); + action->setCheckable(true); + action->setChecked(false); + overlayGroup->addAction(action); + menu->addAction(action); + + menu->addSeparator(); + + action = new QAction(tr("Scroll &Left"), this); + action->setShortcut(tr("Left")); + action->setStatusTip(tr("Scroll the current pane to the left")); + connect(action, SIGNAL(triggered()), this, SLOT(scrollLeft())); + connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(tr("Scroll &Right"), this); + action->setShortcut(tr("Right")); + action->setStatusTip(tr("Scroll the current pane to the right")); + connect(action, SIGNAL(triggered()), this, SLOT(scrollRight())); + connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(tr("Jump Left"), this); + action->setShortcut(tr("Ctrl+Left")); + action->setStatusTip(tr("Scroll the current pane a big step to the left")); + connect(action, SIGNAL(triggered()), this, SLOT(jumpLeft())); + connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(tr("Jump Right"), this); + action->setShortcut(tr("Ctrl+Right")); + action->setStatusTip(tr("Scroll the current pane a big step to the right")); + connect(action, SIGNAL(triggered()), this, SLOT(jumpRight())); + connect(this, SIGNAL(canScroll(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + menu->addSeparator(); + + action = new QAction(QIcon(":/icons/zoom-in.png"), + tr("Zoom &In"), this); + action->setShortcut(tr("Up")); + action->setStatusTip(tr("Increase the zoom level")); + connect(action, SIGNAL(triggered()), this, SLOT(zoomIn())); + connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(QIcon(":/icons/zoom-out.png"), + tr("Zoom &Out"), this); + action->setShortcut(tr("Down")); + action->setStatusTip(tr("Decrease the zoom level")); + connect(action, SIGNAL(triggered()), this, SLOT(zoomOut())); + connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(tr("Restore &Default Zoom"), this); + connect(action, SIGNAL(triggered()), this, SLOT(zoomDefault())); + connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(tr("Zoom to &Fit"), this); + action->setStatusTip(tr("Zoom to show the whole file")); + connect(action, SIGNAL(triggered()), this, SLOT(zoomToFit())); + connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + +/*!!! This one doesn't work properly yet + + menu->addSeparator(); + + action = new QAction(tr("Show &Layer Hierarchy"), this); + action->setShortcut(tr("Alt+L")); + connect(action, SIGNAL(triggered()), this, SLOT(showLayerTree())); + menu->addAction(action); +*/ + } + + if (m_paneMenu) { + m_paneActions.clear(); + m_paneMenu->clear(); + } else { + m_paneMenu = menuBar()->addMenu(tr("&Pane")); + } + + if (m_layerMenu) { + m_layerTransformActions.clear(); + m_layerActions.clear(); + m_layerMenu->clear(); + } else { + m_layerMenu = menuBar()->addMenu(tr("&Layer")); + } + + TransformFactory::TransformList transforms = + TransformFactory::getInstance()->getAllTransforms(); + + std::vector types = + TransformFactory::getInstance()->getAllTransformTypes(); + + std::map transformMenus; + + for (std::vector::iterator i = types.begin(); i != types.end(); ++i) { + transformMenus[*i] = m_layerMenu->addMenu(*i); + m_rightButtonLayerMenu->addMenu(transformMenus[*i]); + } + + for (unsigned int i = 0; i < transforms.size(); ++i) { + + QString description = transforms[i].description; + if (description == "") description = transforms[i].name; + + QString actionText = description; + if (transforms[i].configurable) { + actionText = QString("%1...").arg(actionText); + } + + action = new QAction(actionText, this); + connect(action, SIGNAL(triggered()), this, SLOT(addLayer())); + m_layerTransformActions[action] = transforms[i].name; + connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool))); + transformMenus[transforms[i].type]->addAction(action); + } + + m_rightButtonLayerMenu->addSeparator(); + + menu = m_paneMenu; + + action = new QAction(QIcon(":/icons/pane.png"), tr("Add &New Pane"), this); + action->setShortcut(tr("Alt+N")); + action->setStatusTip(tr("Add a new pane containing only a time ruler")); + connect(action, SIGNAL(triggered()), this, SLOT(addPane())); + connect(this, SIGNAL(canAddPane(bool)), action, SLOT(setEnabled(bool))); + m_paneActions[action] = PaneConfiguration(LayerFactory::TimeRuler); + menu->addAction(action); + + menu->addSeparator(); + + menu = m_layerMenu; + + menu->addSeparator(); + + LayerFactory::LayerTypeSet emptyLayerTypes = + LayerFactory::getInstance()->getValidEmptyLayerTypes(); + + for (LayerFactory::LayerTypeSet::iterator i = emptyLayerTypes.begin(); + i != emptyLayerTypes.end(); ++i) { + + QIcon icon; + QString mainText, tipText, channelText; + LayerFactory::LayerType type = *i; + QString name = LayerFactory::getInstance()->getLayerPresentationName(type); + + icon = QIcon(QString(":/icons/%1.png") + .arg(LayerFactory::getInstance()->getLayerIconName(type))); + + mainText = tr("Add New %1 Layer").arg(name); + tipText = tr("Add a new empty layer of type %1").arg(name); + + action = new QAction(icon, mainText, this); + action->setStatusTip(tipText); + + if (type == LayerFactory::Text) { + action->setShortcut(tr("Alt+T")); + } + + connect(action, SIGNAL(triggered()), this, SLOT(addLayer())); + connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool))); + m_layerActions[action] = type; + menu->addAction(action); + m_rightButtonLayerMenu->addAction(action); + } + + m_rightButtonLayerMenu->addSeparator(); + menu->addSeparator(); + + int channels = 1; + if (getMainModel()) channels = getMainModel()->getChannelCount(); + + if (channels < 1) channels = 1; + + LayerFactory::LayerType backgroundTypes[] = { + LayerFactory::Waveform, + LayerFactory::Spectrogram, + LayerFactory::MelodicRangeSpectrogram, + LayerFactory::PeakFrequencySpectrogram + }; + + for (unsigned int i = 0; + i < sizeof(backgroundTypes)/sizeof(backgroundTypes[0]); ++i) { + + for (int menuType = 0; menuType <= 1; ++menuType) { // pane, layer + + if (menuType == 0) menu = m_paneMenu; + else menu = m_layerMenu; + + QMenu *submenu = 0; + + for (int c = 0; c <= channels; ++c) { + + if (c == 1 && channels == 1) continue; + bool isDefault = (c == 0); + bool isOnly = (isDefault && (channels == 1)); + + if (menuType == 1) { + if (isDefault) isOnly = true; + else continue; + } + + QIcon icon; + QString mainText, shortcutText, tipText, channelText; + LayerFactory::LayerType type = backgroundTypes[i]; + bool mono = true; + + switch (type) { + + case LayerFactory::Waveform: + icon = QIcon(":/icons/waveform.png"); + mainText = tr("Add &Waveform"); + if (menuType == 0) { + shortcutText = tr("Alt+W"); + tipText = tr("Add a new pane showing a waveform view"); + } else { + tipText = tr("Add a new layer showing a waveform view"); + } + mono = false; + break; + + case LayerFactory::Spectrogram: + mainText = tr("Add &Spectrogram"); + if (menuType == 0) { + shortcutText = tr("Alt+S"); + tipText = tr("Add a new pane showing a dB spectrogram"); + } else { + tipText = tr("Add a new layer showing a dB spectrogram"); + } + break; + + case LayerFactory::MelodicRangeSpectrogram: + mainText = tr("Add &Melodic Range Spectrogram"); + if (menuType == 0) { + shortcutText = tr("Alt+M"); + tipText = tr("Add a new pane showing a spectrogram set up for a pitch overview"); + } else { + tipText = tr("Add a new layer showing a spectrogram set up for a pitch overview"); + } + break; + + case LayerFactory::PeakFrequencySpectrogram: + mainText = tr("Add &Peak Frequency Spectrogram"); + if (menuType == 0) { + shortcutText = tr("Alt+P"); + tipText = tr("Add a new pane showing a spectrogram set up for tracking frequencies"); + } else { + tipText = tr("Add a new layer showing a spectrogram set up for tracking frequencies"); + } + break; + + default: break; + } + + if (isOnly) { + + action = new QAction(icon, mainText, this); + action->setShortcut(shortcutText); + action->setStatusTip(tipText); + if (menuType == 0) { + connect(action, SIGNAL(triggered()), this, SLOT(addPane())); + connect(this, SIGNAL(canAddPane(bool)), action, SLOT(setEnabled(bool))); + m_paneActions[action] = PaneConfiguration(type); + } else { + connect(action, SIGNAL(triggered()), this, SLOT(addLayer())); + connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool))); + m_layerActions[action] = type; + } + menu->addAction(action); + + } else { + + QString actionText; + if (c == 0) + if (mono) actionText = tr("&All Channels Mixed"); + else actionText = tr("&All Channels"); + else actionText = tr("Channel &%1").arg(c); + + if (!submenu) { + submenu = menu->addMenu(mainText); + } + + action = new QAction(icon, actionText, this); + if (isDefault) action->setShortcut(shortcutText); + action->setStatusTip(tipText); + if (menuType == 0) { + connect(action, SIGNAL(triggered()), this, SLOT(addPane())); + connect(this, SIGNAL(canAddPane(bool)), action, SLOT(setEnabled(bool))); + m_paneActions[action] = PaneConfiguration(type, c - 1); + } else { + connect(action, SIGNAL(triggered()), this, SLOT(addLayer())); + connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool))); + m_layerActions[action] = type; + } + submenu->addAction(action); + } + } + } + } + + menu = m_paneMenu; + + menu->addSeparator(); + + action = new QAction(QIcon(":/icons/editdelete.png"), tr("&Delete Pane"), this); + action->setShortcut(tr("Alt+D")); + action->setStatusTip(tr("Delete the currently selected pane")); + connect(action, SIGNAL(triggered()), this, SLOT(deleteCurrentPane())); + connect(this, SIGNAL(canDeleteCurrentPane(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + menu = m_layerMenu; + + action = new QAction(QIcon(":/icons/timeruler.png"), tr("Add &Time Ruler"), this); + action->setStatusTip(tr("Add a new layer showing a time ruler")); + connect(action, SIGNAL(triggered()), this, SLOT(addLayer())); + connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool))); + m_layerActions[action] = LayerFactory::TimeRuler; + menu->addAction(action); + + menu->addSeparator(); + + m_existingLayersMenu = menu->addMenu(tr("Add &Existing Layer")); + m_rightButtonLayerMenu->addMenu(m_existingLayersMenu); + setupExistingLayersMenu(); + + m_rightButtonLayerMenu->addSeparator(); + menu->addSeparator(); + + action = new QAction(tr("&Rename Layer..."), this); + action->setShortcut(tr("Alt+R")); + action->setStatusTip(tr("Rename the currently active layer")); + connect(action, SIGNAL(triggered()), this, SLOT(renameCurrentLayer())); + connect(this, SIGNAL(canRenameLayer(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + m_rightButtonLayerMenu->addAction(action); + + action = new QAction(QIcon(":/icons/editdelete.png"), tr("&Delete Layer"), this); + action->setShortcut(tr("Alt+Shift+D")); + action->setStatusTip(tr("Delete the currently active layer")); + connect(action, SIGNAL(triggered()), this, SLOT(deleteCurrentLayer())); + connect(this, SIGNAL(canDeleteCurrentLayer(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + m_rightButtonLayerMenu->addAction(action); + + if (!m_mainMenusCreated) { + + menu = menuBar()->addMenu(tr("&Help")); + + action = new QAction(tr("&Help Reference"), this); + action->setStatusTip(tr("Open the Sonic Visualiser reference manual")); + connect(action, SIGNAL(triggered()), this, SLOT(help())); + menu->addAction(action); + + action = new QAction(tr("Sonic Visualiser on the &Web"), this); + action->setStatusTip(tr("Open the Sonic Visualiser website")); + connect(action, SIGNAL(triggered()), this, SLOT(website())); + menu->addAction(action); + + action = new QAction(tr("&About Sonic Visualiser"), this); + action->setStatusTip(tr("Show information about Sonic Visualiser")); + connect(action, SIGNAL(triggered()), this, SLOT(about())); + menu->addAction(action); +/* + action = new QAction(tr("About &Qt"), this); + action->setStatusTip(tr("Show information about Qt")); + connect(action, SIGNAL(triggered()), + QApplication::getInstance(), SLOT(aboutQt())); + menu->addAction(action); +*/ + } + + m_mainMenusCreated = true; +} + +void +MainWindow::setupRecentFilesMenu() +{ + m_recentFilesMenu->clear(); + std::vector files = RecentFiles::getInstance()->getRecentFiles(); + for (size_t i = 0; i < files.size(); ++i) { + QAction *action = new QAction(files[i], this); + connect(action, SIGNAL(triggered()), this, SLOT(openRecentFile())); + m_recentFilesMenu->addAction(action); + } +} + +void +MainWindow::setupExistingLayersMenu() +{ + if (!m_existingLayersMenu) return; // should have been created by setupMenus + +// std::cerr << "MainWindow::setupExistingLayersMenu" << std::endl; + + m_existingLayersMenu->clear(); + m_existingLayerActions.clear(); + + std::vector orderedLayers; + std::set observedLayers; + + for (int i = 0; i < m_paneStack->getPaneCount(); ++i) { + + Pane *pane = m_paneStack->getPane(i); + if (!pane) continue; + + for (int j = 0; j < pane->getLayerCount(); ++j) { + + Layer *layer = pane->getLayer(j); + if (!layer) continue; + if (observedLayers.find(layer) != observedLayers.end()) { + std::cerr << "found duplicate layer " << layer << std::endl; + continue; + } + +// std::cerr << "found new layer " << layer << " (name = " +// << layer->getLayerPresentationName().toStdString() << ")" << std::endl; + + orderedLayers.push_back(layer); + observedLayers.insert(layer); + } + } + + std::map observedNames; + + for (int i = 0; i < orderedLayers.size(); ++i) { + + QString name = orderedLayers[i]->getLayerPresentationName(); + int n = ++observedNames[name]; + if (n > 1) name = QString("%1 <%2>").arg(name).arg(n); + + QAction *action = new QAction(name, this); + connect(action, SIGNAL(triggered()), this, SLOT(addLayer())); + connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool))); + m_existingLayerActions[action] = orderedLayers[i]; + + m_existingLayersMenu->addAction(action); + } +} + +void +MainWindow::setupToolbars() +{ + QToolBar *toolbar = addToolBar(tr("Transport Toolbar")); + + QAction *action = toolbar->addAction(QIcon(":/icons/rewind-start.png"), + tr("Rewind to Start")); + action->setShortcut(tr("Home")); + action->setStatusTip(tr("Rewind to the start")); + connect(action, SIGNAL(triggered()), this, SLOT(rewindStart())); + connect(this, SIGNAL(canPlay(bool)), action, SLOT(setEnabled(bool))); + + action = toolbar->addAction(QIcon(":/icons/rewind.png"), + tr("Rewind")); + action->setShortcut(tr("PageUp")); + action->setStatusTip(tr("Rewind to the previous time instant in the current layer")); + connect(action, SIGNAL(triggered()), this, SLOT(rewind())); + connect(this, SIGNAL(canRewind(bool)), action, SLOT(setEnabled(bool))); + + action = toolbar->addAction(QIcon(":/icons/playpause.png"), + tr("Play / Pause")); + action->setCheckable(true); + action->setShortcut(tr("Space")); + action->setStatusTip(tr("Start or stop playback from the current position")); + connect(action, SIGNAL(triggered()), this, SLOT(play())); + connect(m_playSource, SIGNAL(playStatusChanged(bool)), + action, SLOT(setChecked(bool))); + connect(this, SIGNAL(canPlay(bool)), action, SLOT(setEnabled(bool))); + + action = toolbar->addAction(QIcon(":/icons/ffwd.png"), + tr("Fast Forward")); + action->setShortcut(tr("PageDown")); + action->setStatusTip(tr("Fast forward to the next time instant in the current layer")); + connect(action, SIGNAL(triggered()), this, SLOT(ffwd())); + connect(this, SIGNAL(canFfwd(bool)), action, SLOT(setEnabled(bool))); + + action = toolbar->addAction(QIcon(":/icons/ffwd-end.png"), + tr("Fast Forward to End")); + action->setShortcut(tr("End")); + action->setStatusTip(tr("Fast-forward to the end")); + connect(action, SIGNAL(triggered()), this, SLOT(ffwdEnd())); + connect(this, SIGNAL(canPlay(bool)), action, SLOT(setEnabled(bool))); + + toolbar = addToolBar(tr("Play Mode Toolbar")); + + action = toolbar->addAction(QIcon(":/icons/playselection.png"), + tr("Constrain Playback to Selection")); + action->setCheckable(true); + action->setChecked(m_viewManager->getPlaySelectionMode()); + action->setShortcut(tr("s")); + action->setStatusTip(tr("Constrain playback to the selected area")); + connect(action, SIGNAL(triggered()), this, SLOT(playSelectionToggled())); + connect(this, SIGNAL(canPlaySelection(bool)), action, SLOT(setEnabled(bool))); + + action = toolbar->addAction(QIcon(":/icons/playloop.png"), + tr("Loop Playback")); + action->setCheckable(true); + action->setChecked(m_viewManager->getPlayLoopMode()); + action->setShortcut(tr("l")); + action->setStatusTip(tr("Loop playback")); + connect(action, SIGNAL(triggered()), this, SLOT(playLoopToggled())); + connect(this, SIGNAL(canPlay(bool)), action, SLOT(setEnabled(bool))); + + toolbar = addToolBar(tr("Edit Toolbar")); + CommandHistory::getInstance()->registerToolbar(toolbar); + + toolbar = addToolBar(tr("Tools Toolbar")); + QActionGroup *group = new QActionGroup(this); + + action = toolbar->addAction(QIcon(":/icons/navigate.png"), + tr("Navigate")); + action->setCheckable(true); + action->setChecked(true); + action->setShortcut(tr("1")); + connect(action, SIGNAL(triggered()), this, SLOT(toolNavigateSelected())); + group->addAction(action); + m_toolActions[ViewManager::NavigateMode] = action; + + action = toolbar->addAction(QIcon(":/icons/select.png"), + tr("Select")); + action->setCheckable(true); + action->setShortcut(tr("2")); + connect(action, SIGNAL(triggered()), this, SLOT(toolSelectSelected())); + group->addAction(action); + m_toolActions[ViewManager::SelectMode] = action; + + action = toolbar->addAction(QIcon(":/icons/move.png"), + tr("Edit")); + action->setCheckable(true); + action->setShortcut(tr("3")); + connect(action, SIGNAL(triggered()), this, SLOT(toolEditSelected())); + connect(this, SIGNAL(canEditLayer(bool)), action, SLOT(setEnabled(bool))); + group->addAction(action); + m_toolActions[ViewManager::EditMode] = action; + + action = toolbar->addAction(QIcon(":/icons/draw.png"), + tr("Draw")); + action->setCheckable(true); + action->setShortcut(tr("4")); + connect(action, SIGNAL(triggered()), this, SLOT(toolDrawSelected())); + connect(this, SIGNAL(canEditLayer(bool)), action, SLOT(setEnabled(bool))); + group->addAction(action); + m_toolActions[ViewManager::DrawMode] = action; + +// action = toolbar->addAction(QIcon(":/icons/text.png"), +// tr("Text")); +// action->setCheckable(true); +// action->setShortcut(tr("5")); +// connect(action, SIGNAL(triggered()), this, SLOT(toolTextSelected())); +// group->addAction(action); +// m_toolActions[ViewManager::TextMode] = action; + + toolNavigateSelected(); +} + +void +MainWindow::updateMenuStates() +{ + bool haveCurrentPane = + (m_paneStack && + (m_paneStack->getCurrentPane() != 0)); + bool haveCurrentLayer = + (haveCurrentPane && + (m_paneStack->getCurrentPane()->getSelectedLayer())); + bool haveMainModel = + (getMainModel() != 0); + bool havePlayTarget = + (m_playTarget != 0); + bool haveSelection = + (m_viewManager && + !m_viewManager->getSelections().empty()); + bool haveCurrentEditableLayer = + (haveCurrentLayer && + m_paneStack->getCurrentPane()->getSelectedLayer()-> + isLayerEditable()); + bool haveCurrentTimeInstantsLayer = + (haveCurrentLayer && + dynamic_cast + (m_paneStack->getCurrentPane()->getSelectedLayer())); + bool haveCurrentTimeValueLayer = + (haveCurrentLayer && + dynamic_cast + (m_paneStack->getCurrentPane()->getSelectedLayer())); + bool haveCurrentColour3DPlot = + (haveCurrentLayer && + dynamic_cast + (m_paneStack->getCurrentPane()->getSelectedLayer())); + bool haveClipboardContents = + (m_viewManager && + !m_viewManager->getClipboard().empty()); + + emit canAddPane(haveMainModel); + emit canDeleteCurrentPane(haveCurrentPane); + emit canZoom(haveMainModel && haveCurrentPane); + emit canScroll(haveMainModel && haveCurrentPane); + emit canAddLayer(haveMainModel && haveCurrentPane); + emit canImportMoreAudio(haveMainModel); + emit canImportLayer(haveMainModel && haveCurrentPane); + emit canExportAudio(haveMainModel); + emit canExportLayer(haveMainModel && + (haveCurrentEditableLayer || haveCurrentColour3DPlot)); + emit canDeleteCurrentLayer(haveCurrentLayer); + emit canRenameLayer(haveCurrentLayer); + emit canEditLayer(haveCurrentEditableLayer); + emit canSelect(haveMainModel && haveCurrentPane); + emit canPlay(/*!!! haveMainModel && */ havePlayTarget); + emit canFfwd(haveCurrentTimeInstantsLayer || haveCurrentTimeValueLayer); + emit canRewind(haveCurrentTimeInstantsLayer || haveCurrentTimeValueLayer); + emit canPaste(haveCurrentEditableLayer && haveClipboardContents); + emit canInsertInstant(haveCurrentPane); + emit canPlaySelection(haveMainModel && havePlayTarget && haveSelection); + emit canClearSelection(haveSelection); + emit canEditSelection(haveSelection && haveCurrentEditableLayer); + emit canSave(m_sessionFile != "" && m_documentModified); +} + +void +MainWindow::updateDescriptionLabel() +{ + if (!getMainModel()) { + m_descriptionLabel->setText(tr("No audio file loaded.")); + return; + } + + QString description; + + size_t ssr = getMainModel()->getSampleRate(); + size_t tsr = ssr; + if (m_playSource) tsr = m_playSource->getTargetSampleRate(); + + if (ssr != tsr) { + description = tr("%1Hz (resampling to %2Hz)").arg(ssr).arg(tsr); + } else { + description = QString("%1Hz").arg(ssr); + } + + description = QString("%1 - %2") + .arg(RealTime::frame2RealTime(getMainModel()->getEndFrame(), ssr) + .toText(false).c_str()) + .arg(description); + + m_descriptionLabel->setText(description); +} + +void +MainWindow::documentModified() +{ +// std::cerr << "MainWindow::documentModified" << std::endl; + + if (!m_documentModified) { + setWindowTitle(tr("%1 (modified)").arg(windowTitle())); + } + + m_documentModified = true; + updateMenuStates(); +} + +void +MainWindow::documentRestored() +{ +// std::cerr << "MainWindow::documentRestored" << std::endl; + + if (m_documentModified) { + QString wt(windowTitle()); + wt.replace(tr(" (modified)"), ""); + setWindowTitle(wt); + } + + m_documentModified = false; + updateMenuStates(); +} + +void +MainWindow::playLoopToggled() +{ + QAction *action = dynamic_cast(sender()); + + if (action) { + m_viewManager->setPlayLoopMode(action->isChecked()); + } else { + m_viewManager->setPlayLoopMode(!m_viewManager->getPlayLoopMode()); + } +} + +void +MainWindow::playSelectionToggled() +{ + QAction *action = dynamic_cast(sender()); + + if (action) { + m_viewManager->setPlaySelectionMode(action->isChecked()); + } else { + m_viewManager->setPlaySelectionMode(!m_viewManager->getPlaySelectionMode()); + } +} + +void +MainWindow::currentPaneChanged(Pane *) +{ + updateMenuStates(); +} + +void +MainWindow::currentLayerChanged(Pane *, Layer *) +{ + updateMenuStates(); +} + +void +MainWindow::toolNavigateSelected() +{ + m_viewManager->setToolMode(ViewManager::NavigateMode); +} + +void +MainWindow::toolSelectSelected() +{ + m_viewManager->setToolMode(ViewManager::SelectMode); +} + +void +MainWindow::toolEditSelected() +{ + m_viewManager->setToolMode(ViewManager::EditMode); +} + +void +MainWindow::toolDrawSelected() +{ + m_viewManager->setToolMode(ViewManager::DrawMode); +} + +//void +//MainWindow::toolTextSelected() +//{ +// m_viewManager->setToolMode(ViewManager::TextMode); +//} + +void +MainWindow::selectAll() +{ + if (!getMainModel()) return; + m_viewManager->setSelection(Selection(getMainModel()->getStartFrame(), + getMainModel()->getEndFrame())); +} + +void +MainWindow::selectToStart() +{ + if (!getMainModel()) return; + m_viewManager->setSelection(Selection(getMainModel()->getStartFrame(), + m_viewManager->getGlobalCentreFrame())); +} + +void +MainWindow::selectToEnd() +{ + if (!getMainModel()) return; + m_viewManager->setSelection(Selection(m_viewManager->getGlobalCentreFrame(), + getMainModel()->getEndFrame())); +} + +void +MainWindow::selectVisible() +{ + Model *model = getMainModel(); + if (!model) return; + + Pane *currentPane = m_paneStack->getCurrentPane(); + if (!currentPane) return; + + size_t startFrame, endFrame; + + if (currentPane->getStartFrame() < 0) startFrame = 0; + else startFrame = currentPane->getStartFrame(); + + if (currentPane->getEndFrame() > model->getEndFrame()) endFrame = model->getEndFrame(); + else endFrame = currentPane->getEndFrame(); + + m_viewManager->setSelection(Selection(startFrame, endFrame)); +} + +void +MainWindow::clearSelection() +{ + m_viewManager->clearSelections(); +} + +void +MainWindow::cut() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (!currentPane) return; + + Layer *layer = currentPane->getSelectedLayer(); + if (!layer) return; + + Clipboard &clipboard = m_viewManager->getClipboard(); + clipboard.clear(); + + MultiSelection::SelectionList selections = m_viewManager->getSelections(); + + CommandHistory::getInstance()->startCompoundOperation(tr("Cut"), true); + + for (MultiSelection::SelectionList::iterator i = selections.begin(); + i != selections.end(); ++i) { + layer->copy(*i, clipboard); + layer->deleteSelection(*i); + } + + CommandHistory::getInstance()->endCompoundOperation(); +} + +void +MainWindow::copy() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (!currentPane) return; + + Layer *layer = currentPane->getSelectedLayer(); + if (!layer) return; + + Clipboard &clipboard = m_viewManager->getClipboard(); + clipboard.clear(); + + MultiSelection::SelectionList selections = m_viewManager->getSelections(); + + for (MultiSelection::SelectionList::iterator i = selections.begin(); + i != selections.end(); ++i) { + layer->copy(*i, clipboard); + } +} + +void +MainWindow::paste() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (!currentPane) return; + + //!!! if we have no current layer, we should create one of the most + // appropriate type + + Layer *layer = currentPane->getSelectedLayer(); + if (!layer) return; + + Clipboard &clipboard = m_viewManager->getClipboard(); + Clipboard::PointList contents = clipboard.getPoints(); +/* + long minFrame = 0; + bool have = false; + for (int i = 0; i < contents.size(); ++i) { + if (!contents[i].haveFrame()) continue; + if (!have || contents[i].getFrame() < minFrame) { + minFrame = contents[i].getFrame(); + have = true; + } + } + + long frameOffset = long(m_viewManager->getGlobalCentreFrame()) - minFrame; + + layer->paste(clipboard, frameOffset); +*/ + layer->paste(clipboard, 0, true); +} + +void +MainWindow::deleteSelected() +{ + if (m_paneStack->getCurrentPane() && + m_paneStack->getCurrentPane()->getSelectedLayer()) { + + MultiSelection::SelectionList selections = + m_viewManager->getSelections(); + + for (MultiSelection::SelectionList::iterator i = selections.begin(); + i != selections.end(); ++i) { + + m_paneStack->getCurrentPane()->getSelectedLayer()->deleteSelection(*i); + } + } +} + +void +MainWindow::insertInstant() +{ + int frame = m_viewManager->getPlaybackFrame(); + + Pane *pane = m_paneStack->getCurrentPane(); + if (!pane) { + return; + } + + Layer *layer = dynamic_cast + (pane->getSelectedLayer()); + + if (!layer) { + for (int i = pane->getLayerCount(); i > 0; --i) { + layer = dynamic_cast(pane->getLayer(i - 1)); + if (layer) break; + } + + if (!layer) { + CommandHistory::getInstance()->startCompoundOperation + (tr("Add Point"), true); + layer = m_document->createEmptyLayer(LayerFactory::TimeInstants); + if (layer) { + m_document->addLayerToView(pane, layer); + m_paneStack->setCurrentLayer(pane, layer); + } + CommandHistory::getInstance()->endCompoundOperation(); + } + } + + if (layer) { + + Model *model = layer->getModel(); + SparseOneDimensionalModel *sodm = dynamic_cast + (model); + + if (sodm) { + SparseOneDimensionalModel::Point point + (frame, QString("%1").arg(sodm->getPointCount() + 1)); + CommandHistory::getInstance()->addCommand + (new SparseOneDimensionalModel::AddPointCommand(sodm, point, + tr("Add Points")), + true, true); // bundled + } + } +} + +void +MainWindow::importAudio() +{ + QString orig = m_audioFile; + +// std::cerr << "orig = " << orig.toStdString() << std::endl; + + if (orig == "") orig = "."; + + QString path = QFileDialog::getOpenFileName + (this, tr("Select an audio file"), orig, + tr("Audio files (%1)\nAll files (*.*)") + .arg(AudioFileReaderFactory::getKnownExtensions())); + + if (path != "") { + if (!openAudioFile(path, ReplaceMainModel)) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("Audio file \"%1\" could not be opened").arg(path)); + } + } +} + +void +MainWindow::importMoreAudio() +{ + QString orig = m_audioFile; + +// std::cerr << "orig = " << orig.toStdString() << std::endl; + + if (orig == "") orig = "."; + + QString path = QFileDialog::getOpenFileName + (this, tr("Select an audio file"), orig, + tr("Audio files (%1)\nAll files (*.*)") + .arg(AudioFileReaderFactory::getKnownExtensions())); + + if (path != "") { + if (!openAudioFile(path, CreateAdditionalModel)) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("Audio file \"%1\" could not be opened").arg(path)); + } + } +} + +void +MainWindow::exportAudio() +{ + if (!getMainModel()) return; + + QString path = QFileDialog::getSaveFileName + (this, tr("Select a file to export to"), ".", + tr("WAV audio files (*.wav)\nAll files (*.*)")); + + if (path == "") return; + + if (!path.endsWith(".wav")) path = path + ".wav"; + + bool ok = false; + QString error; + + WavFileWriter *writer = 0; + MultiSelection ms = m_viewManager->getSelection(); + MultiSelection::SelectionList selections = m_viewManager->getSelections(); + + bool multiple = false; + + if (selections.empty()) { + + writer = new WavFileWriter(path, getMainModel()->getSampleRate(), + getMainModel(), 0); + + } else if (selections.size() == 1) { + + QStringList items; + items << tr("Export the selected region only") + << tr("Export the whole audio file"); + + bool ok = false; + QString item = ListInputDialog::getItem + (this, tr("Select region to export"), + tr("Which region from the original audio file do you want to export?"), + items, 0, &ok); + + if (!ok || item.isEmpty()) return; + + if (item == items[0]) { + + writer = new WavFileWriter(path, getMainModel()->getSampleRate(), + getMainModel(), &ms); + + } else { + + writer = new WavFileWriter(path, getMainModel()->getSampleRate(), + getMainModel(), 0); + } + } else { + + QStringList items; + items << tr("Export the selected regions into a single audio file") + << tr("Export the selected regions into separate files") + << tr("Export the whole audio file"); + + bool ok = false; + QString item = ListInputDialog::getItem + (this, tr("Select region to export"), + tr("Multiple regions of the original audio file are selected.\nWhat do you want to export?"), + items, 0, &ok); + + if (!ok || item.isEmpty()) return; + + if (item == items[0]) { + + writer = new WavFileWriter(path, getMainModel()->getSampleRate(), + getMainModel(), &ms); + + } else if (item == items[2]) { + + writer = new WavFileWriter(path, getMainModel()->getSampleRate(), + getMainModel(), 0); + + } else { + + multiple = true; + + int n = 1; + QString base = path; + base.replace(".wav", ""); + + for (MultiSelection::SelectionList::iterator i = selections.begin(); + i != selections.end(); ++i) { + + MultiSelection subms; + subms.setSelection(*i); + + QString subpath = QString("%1.%2.wav").arg(base).arg(n); + ++n; + + if (QFileInfo(subpath).exists()) { + error = tr("Fragment file %1 already exists, aborting").arg(subpath); + break; + } + + WavFileWriter subwriter(subpath, getMainModel()->getSampleRate(), + getMainModel(), &subms); + subwriter.write(); + ok = subwriter.isOK(); + + if (!ok) { + error = subwriter.getError(); + break; + } + } + } + } + + if (writer) { + writer->write(); + ok = writer->isOK(); + error = writer->getError(); + delete writer; + } + + if (ok) { + if (!multiple) { + RecentFiles::getInstance()->addFile(path); + } + } else { + QMessageBox::critical(this, tr("Failed to write file"), error); + } +} + +void +MainWindow::importLayer() +{ + Pane *pane = m_paneStack->getCurrentPane(); + + if (!pane) { + // shouldn't happen, as the menu action should have been disabled + std::cerr << "WARNING: MainWindow::importLayer: no current pane" << std::endl; + return; + } + + if (!getMainModel()) { + // shouldn't happen, as the menu action should have been disabled + std::cerr << "WARNING: MainWindow::importLayer: No main model -- hence no default sample rate available" << std::endl; + return; + } + + QString path = QFileDialog::getOpenFileName + (this, tr("Select file"), ".", + tr("All supported files (%1)\nSonic Visualiser Layer XML files (*.svl)\nComma-separated data files (*.csv)\nSpace-separated .lab files (*.lab)\nMIDI files (*.mid)\nText files (*.txt)\nAll files (*.*)").arg(DataFileReaderFactory::getKnownExtensions())); + + if (path != "") { + + if (!openLayerFile(path)) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("File %1 could not be opened.").arg(path)); + return; + } + } +} + +bool +MainWindow::openLayerFile(QString path) +{ + Pane *pane = m_paneStack->getCurrentPane(); + + if (!pane) { + // shouldn't happen, as the menu action should have been disabled + std::cerr << "WARNING: MainWindow::openLayerFile: no current pane" << std::endl; + return false; + } + + if (!getMainModel()) { + // shouldn't happen, as the menu action should have been disabled + std::cerr << "WARNING: MainWindow::openLayerFile: No main model -- hence no default sample rate available" << std::endl; + return false; + } + + if (path.endsWith(".svl") || path.endsWith(".xml")) { + + PaneCallback callback(this); + QFile file(path); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + std::cerr << "ERROR: MainWindow::openLayerFile(" + << path.toStdString() + << "): Failed to open file for reading" << std::endl; + return false; + } + + SVFileReader reader(m_document, callback); + reader.setCurrentPane(pane); + + QXmlInputSource inputSource(&file); + reader.parse(inputSource); + + if (!reader.isOK()) { + std::cerr << "ERROR: MainWindow::openLayerFile(" + << path.toStdString() + << "): Failed to read XML file: " + << reader.getErrorString().toStdString() << std::endl; + return false; + } + + RecentFiles::getInstance()->addFile(path); + return true; + + } else { + + Model *model = DataFileReaderFactory::load(path, getMainModel()->getSampleRate()); + + if (model) { + Layer *newLayer = m_document->createImportedLayer(model); + if (newLayer) { + m_document->addLayerToView(pane, newLayer); + RecentFiles::getInstance()->addFile(path); + return true; + } + } + } + + return false; +} + +void +MainWindow::exportLayer() +{ + Pane *pane = m_paneStack->getCurrentPane(); + if (!pane) return; + + Layer *layer = pane->getSelectedLayer(); + if (!layer) return; + + Model *model = layer->getModel(); + if (!model) return; + + QString path = QFileDialog::getSaveFileName + (this, tr("Select a file to export to"), ".", + tr("Sonic Visualiser Layer XML files (*.svl)\nComma-separated data files (*.csv)\nText files (*.txt)\nAll files (*.*)")); + + if (path == "") return; + + if (QFileInfo(path).suffix() == "") path += ".svl"; + + QString error; + + if (path.endsWith(".xml") || path.endsWith(".svl")) { + + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { + error = tr("Failed to open file %1 for writing").arg(path); + } else { + QTextStream out(&file); + out << "\n" + << "\n" + << "\n" + << " \n"; + + model->toXml(out, " "); + + out << " \n" + << " \n"; + + layer->toXml(out, " "); + + out << " \n" + << "\n"; + } + + } else { + + CSVFileWriter writer(path, model, + (path.endsWith(".csv") ? "," : "\t")); + writer.write(); + + if (!writer.isOK()) { + error = writer.getError(); + } + } + + if (error != "") { + QMessageBox::critical(this, tr("Failed to write file"), error); + } else { + RecentFiles::getInstance()->addFile(path); + } +} + +bool +MainWindow::openAudioFile(QString path, AudioFileOpenMode mode) +{ + if (!(QFileInfo(path).exists() && + QFileInfo(path).isFile() && + QFileInfo(path).isReadable())) { + return false; + } + + WaveFileModel *newModel = new WaveFileModel(path); + + if (!newModel->isOK()) { + delete newModel; + return false; + } + + bool setAsMain = true; + static bool prevSetAsMain = true; + + if (mode == CreateAdditionalModel) setAsMain = false; + else if (mode == AskUser) { + if (m_document->getMainModel()) { + + QStringList items; + items << tr("Replace the existing main waveform") + << tr("Load this file into a new waveform pane"); + + bool ok = false; + QString item = ListInputDialog::getItem + (this, tr("Select target for import"), + tr("You already have an audio waveform loaded.\nWhat would you like to do with the new audio file?"), + items, prevSetAsMain ? 0 : 1, &ok); + + if (!ok || item.isEmpty()) { + delete newModel; + return false; + } + + setAsMain = (item == items[0]); + prevSetAsMain = setAsMain; + } + } + + if (setAsMain) { + + Model *prevMain = getMainModel(); + if (prevMain) m_playSource->removeModel(prevMain); + + PlayParameterRepository::getInstance()->clear(); + + // The clear() call will have removed the parameters for the + // main model. Re-add them with the new one. + PlayParameterRepository::getInstance()->addModel(newModel); + + m_document->setMainModel(newModel); + setupMenus(); + + if (m_sessionFile == "") { + setWindowTitle(tr("Sonic Visualiser: %1") + .arg(QFileInfo(path).fileName())); + CommandHistory::getInstance()->clear(); + CommandHistory::getInstance()->documentSaved(); + m_documentModified = false; + } else { + setWindowTitle(tr("Sonic Visualiser: %1 [%2]") + .arg(QFileInfo(m_sessionFile).fileName()) + .arg(QFileInfo(path).fileName())); + if (m_documentModified) { + m_documentModified = false; + documentModified(); // so as to restore "(modified)" window title + } + } + + m_audioFile = path; + + } else { // !setAsMain + + CommandHistory::getInstance()->startCompoundOperation + (tr("Import \"%1\"").arg(QFileInfo(path).fileName()), true); + + m_document->addImportedModel(newModel); + + AddPaneCommand *command = new AddPaneCommand(this); + CommandHistory::getInstance()->addCommand(command); + + Pane *pane = command->getPane(); + + if (!m_timeRulerLayer) { + m_timeRulerLayer = m_document->createMainModelLayer + (LayerFactory::TimeRuler); + } + + m_document->addLayerToView(pane, m_timeRulerLayer); + + Layer *newLayer = m_document->createImportedLayer(newModel); + + if (newLayer) { + m_document->addLayerToView(pane, newLayer); + } + + CommandHistory::getInstance()->endCompoundOperation(); + } + + updateMenuStates(); + RecentFiles::getInstance()->addFile(path); + + return true; +} + +void +MainWindow::createPlayTarget() +{ + if (m_playTarget) return; + + m_playTarget = AudioTargetFactory::createCallbackTarget(m_playSource); + if (!m_playTarget) { + QMessageBox::warning + (this, tr("Couldn't open audio device"), + tr("Could not open an audio device for playback.\nAudio playback will not be available during this session.\n"), + QMessageBox::Ok, 0); + } + connect(m_fader, SIGNAL(valueChanged(float)), + m_playTarget, SLOT(setOutputGain(float))); +} + +WaveFileModel * +MainWindow::getMainModel() +{ + if (!m_document) return 0; + return m_document->getMainModel(); +} + +void +MainWindow::newSession() +{ + if (!checkSaveModified()) return; + + closeSession(); + createDocument(); + + Pane *pane = m_paneStack->addPane(); + + if (!m_timeRulerLayer) { + m_timeRulerLayer = m_document->createMainModelLayer + (LayerFactory::TimeRuler); + } + + m_document->addLayerToView(pane, m_timeRulerLayer); + + Layer *waveform = m_document->createMainModelLayer(LayerFactory::Waveform); + m_document->addLayerToView(pane, waveform); + + m_panner->registerView(pane); + + CommandHistory::getInstance()->clear(); + CommandHistory::getInstance()->documentSaved(); + documentRestored(); + updateMenuStates(); +} + +void +MainWindow::createDocument() +{ + m_document = new Document; + + connect(m_document, SIGNAL(layerAdded(Layer *)), + this, SLOT(layerAdded(Layer *))); + connect(m_document, SIGNAL(layerRemoved(Layer *)), + this, SLOT(layerRemoved(Layer *))); + connect(m_document, SIGNAL(layerAboutToBeDeleted(Layer *)), + this, SLOT(layerAboutToBeDeleted(Layer *))); + connect(m_document, SIGNAL(layerInAView(Layer *, bool)), + this, SLOT(layerInAView(Layer *, bool))); + + connect(m_document, SIGNAL(modelAdded(Model *)), + this, SLOT(modelAdded(Model *))); + connect(m_document, SIGNAL(mainModelChanged(WaveFileModel *)), + this, SLOT(mainModelChanged(WaveFileModel *))); + connect(m_document, SIGNAL(modelAboutToBeDeleted(Model *)), + this, SLOT(modelAboutToBeDeleted(Model *))); + + connect(m_document, SIGNAL(modelGenerationFailed(QString)), + this, SLOT(modelGenerationFailed(QString))); + connect(m_document, SIGNAL(modelRegenerationFailed(QString)), + this, SLOT(modelRegenerationFailed(QString))); +} + +void +MainWindow::closeSession() +{ + if (!checkSaveModified()) return; + + while (m_paneStack->getPaneCount() > 0) { + + Pane *pane = m_paneStack->getPane(m_paneStack->getPaneCount() - 1); + + while (pane->getLayerCount() > 0) { + m_document->removeLayerFromView + (pane, pane->getLayer(pane->getLayerCount() - 1)); + } + + m_panner->unregisterView(pane); + m_paneStack->deletePane(pane); + } + + while (m_paneStack->getHiddenPaneCount() > 0) { + + Pane *pane = m_paneStack->getHiddenPane + (m_paneStack->getHiddenPaneCount() - 1); + + while (pane->getLayerCount() > 0) { + m_document->removeLayerFromView + (pane, pane->getLayer(pane->getLayerCount() - 1)); + } + + m_panner->unregisterView(pane); + m_paneStack->deletePane(pane); + } + + delete m_document; + m_document = 0; + m_viewManager->clearSelections(); + m_timeRulerLayer = 0; // document owned this + + m_sessionFile = ""; + setWindowTitle(tr("Sonic Visualiser")); + + CommandHistory::getInstance()->clear(); + CommandHistory::getInstance()->documentSaved(); + documentRestored(); +} + +void +MainWindow::openSession() +{ + if (!checkSaveModified()) return; + + QString orig = m_audioFile; + if (orig == "") orig = "."; + else orig = QFileInfo(orig).absoluteDir().canonicalPath(); + + QString path = QFileDialog::getOpenFileName + (this, tr("Select a session file"), orig, + tr("Sonic Visualiser session files (*.sv)\nAll files (*.*)")); + + if (path.isEmpty()) return; + + if (!(QFileInfo(path).exists() && + QFileInfo(path).isFile() && + QFileInfo(path).isReadable())) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("File \"%1\" does not exist or is not a readable file").arg(path)); + return; + } + + if (!openSessionFile(path)) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("Session file \"%1\" could not be opened").arg(path)); + } +} + +void +MainWindow::openSomething() +{ + QString orig = m_audioFile; + if (orig == "") orig = "."; + else orig = QFileInfo(orig).absoluteDir().canonicalPath(); + + bool canImportLayer = (getMainModel() != 0 && + m_paneStack != 0 && + m_paneStack->getCurrentPane() != 0); + + QString importSpec; + + if (canImportLayer) { + importSpec = tr("All supported files (*.sv %1 %2)\nSonic Visualiser session files (*.sv)\nAudio files (%1)\nLayer files (%2)\nAll files (*.*)") + .arg(AudioFileReaderFactory::getKnownExtensions()) + .arg(DataFileReaderFactory::getKnownExtensions()); + } else { + importSpec = tr("All supported files (*.sv %1)\nSonic Visualiser session files (*.sv)\nAudio files (%1)\nAll files (*.*)") + .arg(AudioFileReaderFactory::getKnownExtensions()); + } + + QString path = QFileDialog::getOpenFileName + (this, tr("Select a file to open"), orig, importSpec); + + if (path.isEmpty()) return; + + if (!(QFileInfo(path).exists() && + QFileInfo(path).isFile() && + QFileInfo(path).isReadable())) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("File \"%1\" does not exist or is not a readable file").arg(path)); + return; + } + + if (path.endsWith(".sv")) { + + if (!checkSaveModified()) return; + + if (!openSessionFile(path)) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("Session file \"%1\" could not be opened").arg(path)); + } + + } else { + + if (!openAudioFile(path, AskUser)) { + + if (!canImportLayer || !openLayerFile(path)) { + + QMessageBox::critical(this, tr("Failed to open file"), + tr("File \"%1\" could not be opened").arg(path)); + } + } + } +} + +void +MainWindow::openRecentFile() +{ + QObject *obj = sender(); + QAction *action = dynamic_cast(obj); + + if (!action) { + std::cerr << "WARNING: MainWindow::openRecentFile: sender is not an action" + << std::endl; + return; + } + + QString path = action->text(); + if (path == "") return; + + if (path.endsWith("sv")) { + + if (!checkSaveModified()) return ; + + if (!openSessionFile(path)) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("Session file \"%1\" could not be opened").arg(path)); + } + + } else { + + if (!openAudioFile(path, AskUser)) { + + bool canImportLayer = (getMainModel() != 0 && + m_paneStack != 0 && + m_paneStack->getCurrentPane() != 0); + + if (!canImportLayer || !openLayerFile(path)) { + + QMessageBox::critical(this, tr("Failed to open file"), + tr("File \"%1\" could not be opened").arg(path)); + } + } + } +} + +bool +MainWindow::openSomeFile(QString path) +{ + if (openAudioFile(path)) { + return true; + } else if (openSessionFile(path)) { + return true; + } else { + return false; + } +} + +bool +MainWindow::openSessionFile(QString path) +{ + BZipFileDevice bzFile(path); + if (!bzFile.open(QIODevice::ReadOnly)) { + std::cerr << "Failed to open session file \"" << path.toStdString() + << "\": " << bzFile.errorString().toStdString() << std::endl; + return false; + } + + QString error; + closeSession(); + createDocument(); + + PaneCallback callback(this); + m_viewManager->clearSelections(); + + SVFileReader reader(m_document, callback); + QXmlInputSource inputSource(&bzFile); + reader.parse(inputSource); + + if (!reader.isOK()) { + error = tr("SV XML file read error:\n%1").arg(reader.getErrorString()); + } + + bzFile.close(); + + bool ok = (error == ""); + + if (ok) { + setWindowTitle(tr("Sonic Visualiser: %1") + .arg(QFileInfo(path).fileName())); + m_sessionFile = path; + setupMenus(); + CommandHistory::getInstance()->clear(); + CommandHistory::getInstance()->documentSaved(); + m_documentModified = false; + updateMenuStates(); + RecentFiles::getInstance()->addFile(path); + } else { + setWindowTitle(tr("Sonic Visualiser")); + } + + return ok; +} + +void +MainWindow::closeEvent(QCloseEvent *e) +{ + if (!checkSaveModified()) { + e->ignore(); + return; + } + + e->accept(); + return; +} + +bool +MainWindow::checkSaveModified() +{ + // Called before some destructive operation (e.g. new session, + // exit program). Return true if we can safely proceed, false to + // cancel. + + if (!m_documentModified) return true; + + int button = + QMessageBox::warning(this, + tr("Session modified"), + tr("The current session has been modified.\nDo you want to save it?"), + QMessageBox::Yes, + QMessageBox::No, + QMessageBox::Cancel); + + if (button == QMessageBox::Yes) { + saveSession(); + if (m_documentModified) { // save failed -- don't proceed! + return false; + } else { + return true; // saved, so it's safe to continue now + } + } else if (button == QMessageBox::No) { + m_documentModified = false; // so we know to abandon it + return true; + } + + // else cancel + return false; +} + +void +MainWindow::saveSession() +{ + if (m_sessionFile != "") { + if (!saveSessionFile(m_sessionFile)) { + QMessageBox::critical(this, tr("Failed to save file"), + tr("Session file \"%1\" could not be saved.").arg(m_sessionFile)); + } else { + CommandHistory::getInstance()->documentSaved(); + documentRestored(); + } + } else { + saveSessionAs(); + } +} + +void +MainWindow::saveSessionAs() +{ + QString orig = m_audioFile; + if (orig == "") orig = "."; + else orig = QFileInfo(orig).absoluteDir().canonicalPath(); + + QString path; + bool good = false; + + while (!good) { + + path = QFileDialog::getSaveFileName + (this, tr("Select a file to save to"), orig, + tr("Sonic Visualiser session files (*.sv)\nAll files (*.*)"), 0, + QFileDialog::DontConfirmOverwrite); // we'll do that + + if (path.isEmpty()) return; + + if (!path.endsWith(".sv")) path = path + ".sv"; + + QFileInfo fi(path); + + if (fi.isDir()) { + QMessageBox::critical(this, tr("Directory selected"), + tr("File \"%1\" is a directory").arg(path)); + continue; + } + + if (fi.exists()) { + if (QMessageBox::question(this, tr("File exists"), + tr("The file \"%1\" already exists.\nDo you want to overwrite it?").arg(path), + QMessageBox::Ok, + QMessageBox::Cancel) == QMessageBox::Ok) { + good = true; + } else { + continue; + } + } + + good = true; + } + + if (!saveSessionFile(path)) { + QMessageBox::critical(this, tr("Failed to save file"), + tr("Session file \"%1\" could not be saved.").arg(m_sessionFile)); + } else { + setWindowTitle(tr("Sonic Visualiser: %1") + .arg(QFileInfo(path).fileName())); + m_sessionFile = path; + CommandHistory::getInstance()->documentSaved(); + documentRestored(); + RecentFiles::getInstance()->addFile(path); + } +} + +bool +MainWindow::saveSessionFile(QString path) +{ + BZipFileDevice bzFile(path); + if (!bzFile.open(QIODevice::WriteOnly)) { + std::cerr << "Failed to open session file \"" << path.toStdString() + << "\" for writing: " + << bzFile.errorString().toStdString() << std::endl; + return false; + } + + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + + QTextStream out(&bzFile); + toXml(out); + out.flush(); + + QApplication::restoreOverrideCursor(); + + if (bzFile.errorString() != "") { + QMessageBox::critical(this, tr("Failed to write file"), + tr("Failed to write to file \"%1\": %2") + .arg(path).arg(bzFile.errorString())); + bzFile.close(); + return false; + } + + bzFile.close(); + return true; +} + +void +MainWindow::toXml(QTextStream &out) +{ + QString indent(" "); + + out << "\n"; + out << "\n"; + out << "\n"; + + m_document->toXml(out, "", ""); + + out << "\n"; + + out << QString(" \n") + .arg(width()).arg(height()); + + for (int i = 0; i < m_paneStack->getPaneCount(); ++i) { + + Pane *pane = m_paneStack->getPane(i); + + if (pane) { + pane->toXml(out, indent); + } + } + + out << "\n"; + + m_viewManager->getSelection().toXml(out); + + out << "\n"; +} + +Pane * +MainWindow::addPaneToStack() +{ + AddPaneCommand *command = new AddPaneCommand(this); + CommandHistory::getInstance()->addCommand(command); + return command->getPane(); +} + +void +MainWindow::zoomIn() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (currentPane) currentPane->zoom(true); +} + +void +MainWindow::zoomOut() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (currentPane) currentPane->zoom(false); +} + +void +MainWindow::zoomToFit() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (!currentPane) return; + + Model *model = getMainModel(); + if (!model) return; + + size_t start = model->getStartFrame(); + size_t end = model->getEndFrame(); + size_t pixels = currentPane->width(); + size_t zoomLevel = (end - start) / pixels; + + currentPane->setZoomLevel(zoomLevel); + currentPane->setStartFrame(start); +} + +void +MainWindow::zoomDefault() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (currentPane) currentPane->setZoomLevel(1024); +} + +void +MainWindow::scrollLeft() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (currentPane) currentPane->scroll(false, false); +} + +void +MainWindow::jumpLeft() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (currentPane) currentPane->scroll(false, true); +} + +void +MainWindow::scrollRight() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (currentPane) currentPane->scroll(true, false); +} + +void +MainWindow::jumpRight() +{ + Pane *currentPane = m_paneStack->getCurrentPane(); + if (currentPane) currentPane->scroll(true, true); +} + +void +MainWindow::showNoOverlays() +{ + m_viewManager->setOverlayMode(ViewManager::NoOverlays); +} + +void +MainWindow::showBasicOverlays() +{ + m_viewManager->setOverlayMode(ViewManager::BasicOverlays); +} + +void +MainWindow::showAllOverlays() +{ + m_viewManager->setOverlayMode(ViewManager::AllOverlays); +} + +void +MainWindow::play() +{ + if (m_playSource->isPlaying()) { + m_playSource->stop(); + } else { + m_playSource->play(m_viewManager->getPlaybackFrame()); + } +} + +void +MainWindow::ffwd() +{ + if (!getMainModel()) return; + + int frame = m_viewManager->getPlaybackFrame(); + ++frame; + + Pane *pane = m_paneStack->getCurrentPane(); + if (!pane) return; + + Layer *layer = pane->getSelectedLayer(); + + if (!dynamic_cast(layer) && + !dynamic_cast(layer)) return; + + size_t resolution = 0; + if (!layer->snapToFeatureFrame(pane, frame, resolution, Layer::SnapRight)) { + frame = getMainModel()->getEndFrame(); + } + + m_viewManager->setPlaybackFrame(frame); +} + +void +MainWindow::ffwdEnd() +{ + if (!getMainModel()) return; + m_viewManager->setPlaybackFrame(getMainModel()->getEndFrame()); +} + +void +MainWindow::rewind() +{ + if (!getMainModel()) return; + + int frame = m_viewManager->getPlaybackFrame(); + if (frame > 0) --frame; + + Pane *pane = m_paneStack->getCurrentPane(); + if (!pane) return; + + Layer *layer = pane->getSelectedLayer(); + + if (!dynamic_cast(layer) && + !dynamic_cast(layer)) return; + + size_t resolution = 0; + if (!layer->snapToFeatureFrame(pane, frame, resolution, Layer::SnapLeft)) { + frame = getMainModel()->getEndFrame(); + } + + m_viewManager->setPlaybackFrame(frame); +} + +void +MainWindow::rewindStart() +{ + if (!getMainModel()) return; + m_viewManager->setPlaybackFrame(getMainModel()->getStartFrame()); +} + +void +MainWindow::stop() +{ + m_playSource->stop(); +} + +void +MainWindow::addPane() +{ + QObject *s = sender(); + QAction *action = dynamic_cast(s); + + if (!action) { + std::cerr << "WARNING: MainWindow::addPane: sender is not an action" + << std::endl; + return; + } + + PaneActionMap::iterator i = m_paneActions.find(action); + + if (i == m_paneActions.end()) { + std::cerr << "WARNING: MainWindow::addPane: unknown action " + << action->objectName().toStdString() << std::endl; + return; + } + + CommandHistory::getInstance()->startCompoundOperation + (action->text(), true); + + AddPaneCommand *command = new AddPaneCommand(this); + CommandHistory::getInstance()->addCommand(command); + + Pane *pane = command->getPane(); + + if (i->second.layer != LayerFactory::TimeRuler) { + if (!m_timeRulerLayer) { +// std::cerr << "no time ruler layer, creating one" << std::endl; + m_timeRulerLayer = m_document->createMainModelLayer + (LayerFactory::TimeRuler); + } + +// std::cerr << "adding time ruler layer " << m_timeRulerLayer << std::endl; + + m_document->addLayerToView(pane, m_timeRulerLayer); + } + + Layer *newLayer = m_document->createLayer(i->second.layer); + m_document->setModel(newLayer, m_document->getMainModel()); + m_document->setChannel(newLayer, i->second.channel); + m_document->addLayerToView(pane, newLayer); + + m_paneStack->setCurrentPane(pane); + + CommandHistory::getInstance()->endCompoundOperation(); + + updateMenuStates(); +} + +MainWindow::AddPaneCommand::AddPaneCommand(MainWindow *mw) : + m_mw(mw), + m_pane(0), + m_prevCurrentPane(0), + m_added(false) +{ +} + +MainWindow::AddPaneCommand::~AddPaneCommand() +{ + if (m_pane && !m_added) { + m_mw->m_paneStack->deletePane(m_pane); + } +} + +QString +MainWindow::AddPaneCommand::getName() const +{ + return tr("Add Pane"); +} + +void +MainWindow::AddPaneCommand::execute() +{ + if (!m_pane) { + m_prevCurrentPane = m_mw->m_paneStack->getCurrentPane(); + m_pane = m_mw->m_paneStack->addPane(); + } else { + m_mw->m_paneStack->showPane(m_pane); + } + + m_mw->m_paneStack->setCurrentPane(m_pane); + m_mw->m_panner->registerView(m_pane); + m_added = true; +} + +void +MainWindow::AddPaneCommand::unexecute() +{ + m_mw->m_paneStack->hidePane(m_pane); + m_mw->m_paneStack->setCurrentPane(m_prevCurrentPane); + m_mw->m_panner->unregisterView(m_pane); + m_added = false; +} + +MainWindow::RemovePaneCommand::RemovePaneCommand(MainWindow *mw, Pane *pane) : + m_mw(mw), + m_pane(pane), + m_added(true) +{ +} + +MainWindow::RemovePaneCommand::~RemovePaneCommand() +{ + if (m_pane && !m_added) { + m_mw->m_paneStack->deletePane(m_pane); + } +} + +QString +MainWindow::RemovePaneCommand::getName() const +{ + return tr("Remove Pane"); +} + +void +MainWindow::RemovePaneCommand::execute() +{ + m_prevCurrentPane = m_mw->m_paneStack->getCurrentPane(); + m_mw->m_paneStack->hidePane(m_pane); + m_mw->m_panner->unregisterView(m_pane); + m_added = false; +} + +void +MainWindow::RemovePaneCommand::unexecute() +{ + m_mw->m_paneStack->showPane(m_pane); + m_mw->m_paneStack->setCurrentPane(m_prevCurrentPane); + m_mw->m_panner->registerView(m_pane); + m_added = true; +} + +void +MainWindow::addLayer() +{ + QObject *s = sender(); + QAction *action = dynamic_cast(s); + + if (!action) { + std::cerr << "WARNING: MainWindow::addLayer: sender is not an action" + << std::endl; + return; + } + + Pane *pane = m_paneStack->getCurrentPane(); + + if (!pane) { + std::cerr << "WARNING: MainWindow::addLayer: no current pane" << std::endl; + return; + } + + ExistingLayerActionMap::iterator ei = m_existingLayerActions.find(action); + + if (ei != m_existingLayerActions.end()) { + Layer *newLayer = ei->second; + m_document->addLayerToView(pane, newLayer); + m_paneStack->setCurrentLayer(pane, newLayer); + return; + } + + TransformActionMap::iterator i = m_layerTransformActions.find(action); + + if (i == m_layerTransformActions.end()) { + + LayerActionMap::iterator i = m_layerActions.find(action); + + if (i == m_layerActions.end()) { + std::cerr << "WARNING: MainWindow::addLayer: unknown action " + << action->objectName().toStdString() << std::endl; + return; + } + + LayerFactory::LayerType type = i->second; + + LayerFactory::LayerTypeSet emptyTypes = + LayerFactory::getInstance()->getValidEmptyLayerTypes(); + + Layer *newLayer; + + if (emptyTypes.find(type) != emptyTypes.end()) { + + newLayer = m_document->createEmptyLayer(type); + m_toolActions[ViewManager::DrawMode]->trigger(); + + } else { + + newLayer = m_document->createMainModelLayer(type); + } + + m_document->addLayerToView(pane, newLayer); + m_paneStack->setCurrentLayer(pane, newLayer); + + return; + } + + TransformName transform = i->second; + TransformFactory *factory = TransformFactory::getInstance(); + + QString configurationXml; + + int channel = -1; + // pick up the default channel from any existing layers on the same pane + for (int j = 0; j < pane->getLayerCount(); ++j) { + int c = LayerFactory::getInstance()->getChannel(pane->getLayer(j)); + if (c != -1) { + channel = c; + break; + } + } + + bool needConfiguration = false; + + if (factory->isTransformConfigurable(transform)) { + needConfiguration = true; + } else { + int minChannels, maxChannels; + int myChannels = m_document->getMainModel()->getChannelCount(); + if (factory->getTransformChannelRange(transform, + minChannels, + maxChannels)) { +// std::cerr << "myChannels: " << myChannels << ", minChannels: " << minChannels << ", maxChannels: " << maxChannels << std::endl; + needConfiguration = (myChannels > maxChannels && maxChannels == 1); + } + } + + if (needConfiguration) { + bool ok = + factory->getConfigurationForTransform + (transform, m_document->getMainModel(), channel, configurationXml); + if (!ok) return; + } + + Layer *newLayer = m_document->createDerivedLayer(transform, + m_document->getMainModel(), + channel, + configurationXml); + + if (newLayer) { + m_document->addLayerToView(pane, newLayer); + m_document->setChannel(newLayer, channel); + } + + updateMenuStates(); +} + +void +MainWindow::deleteCurrentPane() +{ + CommandHistory::getInstance()->startCompoundOperation + (tr("Delete Pane"), true); + + Pane *pane = m_paneStack->getCurrentPane(); + if (pane) { + while (pane->getLayerCount() > 0) { + Layer *layer = pane->getLayer(0); + if (layer) { + m_document->removeLayerFromView(pane, layer); + } else { + break; + } + } + + RemovePaneCommand *command = new RemovePaneCommand(this, pane); + CommandHistory::getInstance()->addCommand(command); + } + + CommandHistory::getInstance()->endCompoundOperation(); + + updateMenuStates(); +} + +void +MainWindow::deleteCurrentLayer() +{ + Pane *pane = m_paneStack->getCurrentPane(); + if (pane) { + Layer *layer = pane->getSelectedLayer(); + if (layer) { + m_document->removeLayerFromView(pane, layer); + } + } + updateMenuStates(); +} + +void +MainWindow::renameCurrentLayer() +{ + Pane *pane = m_paneStack->getCurrentPane(); + if (pane) { + Layer *layer = pane->getSelectedLayer(); + if (layer) { + bool ok = false; + QString newName = QInputDialog::getText + (this, tr("Rename Layer"), + tr("New name for this layer:"), + QLineEdit::Normal, layer->objectName(), &ok); + if (ok) { + layer->setObjectName(newName); + setupExistingLayersMenu(); + } + } + } +} + +void +MainWindow::playSpeedChanged(int speed) +{ + int factor = 11 - speed; + m_playSpeed->setToolTip(tr("Playback speed: %1") + .arg(factor > 1 ? + QString("1/%1").arg(factor) : + tr("Full"))); + m_playSource->setSlowdownFactor(factor); +} + +void +MainWindow::outputLevelsChanged(float left, float right) +{ + m_fader->setPeakLeft(left); + m_fader->setPeakRight(right); +} + +void +MainWindow::sampleRateMismatch(size_t requested, size_t actual, + bool willResample) +{ + if (!willResample) { + //!!! more helpful message needed + QMessageBox::information + (this, tr("Sample rate mismatch"), + tr("The sample rate of this audio file (%1 Hz) does not match\nthe current playback rate (%2 Hz).\n\nThe file will play at the wrong speed.") + .arg(requested).arg(actual)); + } + +/*!!! Let's not do this for now, and see how we go -- now that we're putting + sample rate information in the status bar + + QMessageBox::information + (this, tr("Sample rate mismatch"), + tr("The sample rate of this audio file (%1 Hz) does not match\nthat of the output audio device (%2 Hz).\n\nThe file will be resampled automatically during playback.") + .arg(requested).arg(actual)); +*/ + + updateDescriptionLabel(); +} + +void +MainWindow::layerAdded(Layer *layer) +{ +// std::cerr << "MainWindow::layerAdded(" << layer << ")" << std::endl; +// setupExistingLayersMenu(); + updateMenuStates(); +} + +void +MainWindow::layerRemoved(Layer *layer) +{ +// std::cerr << "MainWindow::layerRemoved(" << layer << ")" << std::endl; + setupExistingLayersMenu(); + updateMenuStates(); +} + +void +MainWindow::layerAboutToBeDeleted(Layer *layer) +{ +// std::cerr << "MainWindow::layerAboutToBeDeleted(" << layer << ")" << std::endl; + if (layer == m_timeRulerLayer) { +// std::cerr << "(this is the time ruler layer)" << std::endl; + m_timeRulerLayer = 0; + } +} + +void +MainWindow::layerInAView(Layer *layer, bool inAView) +{ +// std::cerr << "MainWindow::layerInAView(" << layer << "," << inAView << ")" << std::endl; + + // Check whether we need to add or remove model from play source + Model *model = layer->getModel(); + if (model) { + if (inAView) { + m_playSource->addModel(model); + } else { + bool found = false; + for (int i = 0; i < m_paneStack->getPaneCount(); ++i) { + Pane *pane = m_paneStack->getPane(i); + if (!pane) continue; + for (int j = 0; j < pane->getLayerCount(); ++j) { + Layer *pl = pane->getLayer(j); + if (pl && pl->getModel() == model) { + found = true; + break; + } + } + if (found) break; + } + if (!found) m_playSource->removeModel(model); + } + } + + setupExistingLayersMenu(); + updateMenuStates(); +} + +void +MainWindow::modelAdded(Model *model) +{ +// std::cerr << "MainWindow::modelAdded(" << model << ")" << std::endl; + m_playSource->addModel(model); +} + +void +MainWindow::mainModelChanged(WaveFileModel *model) +{ +// std::cerr << "MainWindow::mainModelChanged(" << model << ")" << std::endl; + updateDescriptionLabel(); + m_panLayer->setModel(model); + if (model) m_viewManager->setMainModelSampleRate(model->getSampleRate()); + if (model && !m_playTarget) createPlayTarget(); +} + +void +MainWindow::modelAboutToBeDeleted(Model *model) +{ +// std::cerr << "MainWindow::modelAboutToBeDeleted(" << model << ")" << std::endl; + m_playSource->removeModel(model); +} + +void +MainWindow::modelGenerationFailed(QString transformName) +{ + QMessageBox::warning + (this, + tr("Failed to generate layer"), + tr("The layer transform \"%1\" failed to run.\nThis probably means that a plugin failed to initialise.") + .arg(transformName), + QMessageBox::Ok, 0); +} + +void +MainWindow::modelRegenerationFailed(QString layerName, QString transformName) +{ + QMessageBox::warning + (this, + tr("Failed to regenerate layer"), + tr("Failed to regenerate derived layer \"%1\".\nThe layer transform \"%2\" failed to run.\nThis probably means the layer used a plugin that is not currently available.") + .arg(layerName).arg(transformName), + QMessageBox::Ok, 0); +} + +void +MainWindow::rightButtonMenuRequested(Pane *pane, QPoint position) +{ +// std::cerr << "MainWindow::rightButtonMenuRequested(" << pane << ", " << position.x() << ", " << position.y() << ")" << std::endl; + m_paneStack->setCurrentPane(pane); + m_rightButtonMenu->popup(position); +} + +void +MainWindow::showLayerTree() +{ + QTreeView *view = new QTreeView(); + LayerTreeModel *tree = new LayerTreeModel(m_paneStack); + view->expand(tree->index(0, 0, QModelIndex())); + view->setModel(tree); + view->show(); +} + +void +MainWindow::preferenceChanged(PropertyContainer::PropertyName name) +{ + if (name == "Property Box Layout") { + if (Preferences::getInstance()->getPropertyBoxLayout() == + Preferences::VerticallyStacked) { + m_paneStack->setLayoutStyle(PaneStack::PropertyStackPerPaneLayout); + } else { + m_paneStack->setLayoutStyle(PaneStack::SinglePropertyStackLayout); + } + } +} + +void +MainWindow::preferences() +{ + if (!m_preferencesDialog.isNull()) { + m_preferencesDialog->show(); + m_preferencesDialog->raise(); + return; + } + + m_preferencesDialog = new PreferencesDialog(this); + + // DeleteOnClose is safe here, because m_preferencesDialog is a + // QPointer that will be zeroed when the dialog is deleted. We + // use it in preference to leaving the dialog lying around because + // if you Cancel the dialog, it resets the preferences state + // without resetting its own widgets, so its state will be + // incorrect when next shown unless we construct it afresh + m_preferencesDialog->setAttribute(Qt::WA_DeleteOnClose); + + m_preferencesDialog->show(); +} + +void +MainWindow::website() +{ + openHelpUrl(tr("http://www.sonicvisualiser.org/")); +} + +void +MainWindow::help() +{ + openHelpUrl(tr("http://www.sonicvisualiser.org/doc/reference/en/")); +} + +void +MainWindow::openHelpUrl(QString url) +{ + // This method mostly lifted from Qt Assistant source code + + QProcess *process = new QProcess(this); + connect(process, SIGNAL(finished(int)), process, SLOT(deleteLater())); + + QStringList args; + +#ifdef Q_OS_MAC + args.append(url); + process->start("open", args); +#else +#ifdef Q_OS_WIN32 + + QString pf(getenv("ProgramFiles")); + QString command = pf + QString("\\Internet Explorer\\IEXPLORE.EXE"); + + args.append(url); + process->start(command, args); + +#else +#ifdef Q_WS_X11 + if (!qgetenv("KDE_FULL_SESSION").isEmpty()) { + args.append("exec"); + args.append(url); + process->start("kfmclient", args); + } else if (!qgetenv("BROWSER").isEmpty()) { + args.append(url); + process->start(qgetenv("BROWSER"), args); + } else { + args.append(url); + process->start("firefox", args); + } +#endif +#endif +#endif +} + +void +MainWindow::about() +{ + bool debug = false; + QString version = "(unknown version)"; + +#ifdef BUILD_DEBUG + debug = true; +#endif +#ifdef SV_VERSION +#ifdef SVNREV + version = tr("Release %1 : Revision %2").arg(SV_VERSION).arg(SVNREV); +#else + version = tr("Release %1").arg(SV_VERSION); +#endif +#else +#ifdef SVNREV + version = tr("Unreleased : Revision %1").arg(SVNREV); +#endif +#endif + + QString aboutText; + + aboutText += tr("

About Sonic Visualiser

"); + aboutText += tr("

Sonic Visualiser is a program for viewing and exploring audio data for semantic music analysis and annotation.

"); + aboutText += tr("

%1 : %2 build

") + .arg(version) + .arg(debug ? tr("Debug") : tr("Release")); + +#ifdef BUILD_STATIC + aboutText += tr("

Statically linked"); +#ifndef QT_SHARED + aboutText += tr("
With Qt (v%1) © Trolltech AS").arg(QT_VERSION_STR); +#endif +#ifdef HAVE_JACK + aboutText += tr("
With JACK audio output (v%1) © Paul Davis and Jack O'Quin").arg(JACK_VERSION); +#endif +#ifdef HAVE_PORTAUDIO + aboutText += tr("
With PortAudio audio output © Ross Bencina and Phil Burk"); +#endif +#ifdef HAVE_OGGZ + aboutText += tr("
With Ogg file decoder (oggz v%1, fishsound v%2) © CSIRO Australia").arg(OGGZ_VERSION).arg(FISHSOUND_VERSION); +#endif +#ifdef HAVE_MAD + aboutText += tr("
With MAD mp3 decoder (v%1) © Underbit Technologies Inc").arg(MAD_VERSION); +#endif +#ifdef HAVE_SAMPLERATE + aboutText += tr("
With libsamplerate (v%1) © Erik de Castro Lopo").arg(SAMPLERATE_VERSION); +#endif +#ifdef HAVE_SNDFILE + aboutText += tr("
With libsndfile (v%1) © Erik de Castro Lopo").arg(SNDFILE_VERSION); +#endif +#ifdef HAVE_FFTW3 + aboutText += tr("
With FFTW3 (v%1) © Matteo Frigo and MIT").arg(FFTW3_VERSION); +#endif +#ifdef HAVE_VAMP + aboutText += tr("
With Vamp plugin support (API v%1, SDK v%2) © Chris Cannam").arg(VAMP_API_VERSION).arg(VAMP_SDK_VERSION); +#endif + aboutText += tr("
With LADSPA plugin support (API v%1) © Richard Furse, Paul Davis, Stefan Westerfeld").arg(LADSPA_VERSION); + aboutText += tr("
With DSSI plugin support (API v%1) © Chris Cannam, Steve Harris, Sean Bolton").arg(DSSI_VERSION); + aboutText += "

"; +#endif + + aboutText += + "

Sonic Visualiser Copyright © 2005 - 2006 Chris Cannam
" + "Centre for Digital Music, Queen Mary, University of London.

" + "

This program 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.
See the file " + "COPYING included with this distribution for more information.

"; + + QMessageBox::about(this, tr("About Sonic Visualiser"), aboutText); +} + + +#ifdef INCLUDE_MOCFILES +#include "MainWindow.moc.cpp" +#endif diff -r 000000000000 -r cd5d7ff8ef38 main/MainWindow.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main/MainWindow.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,315 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _MAIN_WINDOW_H_ +#define _MAIN_WINDOW_H_ + +#include +#include +#include +#include + +#include "base/Command.h" +#include "base/ViewManager.h" +#include "base/PropertyContainer.h" +#include "layer/LayerFactory.h" +#include "transform/Transform.h" +#include "fileio/SVFileReader.h" +#include + +class Document; +class PaneStack; +class Pane; +class View; +class Fader; +class Panner; +class Layer; +class WaveformLayer; +class WaveFileModel; +class AudioCallbackPlaySource; +class AudioCallbackPlayTarget; +class CommandHistory; +class QMenu; +class AudioDial; +class QLabel; +class PreferencesDialog; + + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(); + virtual ~MainWindow(); + + enum AudioFileOpenMode { + ReplaceMainModel, + CreateAdditionalModel, + AskUser + }; + + bool openSomeFile(QString path); + bool openAudioFile(QString path, AudioFileOpenMode = AskUser); + bool openLayerFile(QString path); + bool openSessionFile(QString path); + bool saveSessionFile(QString path); + +signals: + // Used to toggle the availability of menu actions + void canAddPane(bool); + void canDeleteCurrentPane(bool); + void canAddLayer(bool); + void canImportMoreAudio(bool); + void canImportLayer(bool); + void canExportAudio(bool); + void canExportLayer(bool); + void canRenameLayer(bool); + void canEditLayer(bool); + void canSelect(bool); + void canClearSelection(bool); + void canEditSelection(bool); + void canPaste(bool); + void canInsertInstant(bool); + void canDeleteCurrentLayer(bool); + void canZoom(bool); + void canScroll(bool); + void canPlay(bool); + void canFfwd(bool); + void canRewind(bool); + void canPlaySelection(bool); + void canSave(bool); + +protected slots: + void openSession(); + void importAudio(); + void importMoreAudio(); + void openSomething(); + void openRecentFile(); + void exportAudio(); + void importLayer(); + void exportLayer(); + void saveSession(); + void saveSessionAs(); + void newSession(); + void closeSession(); + void preferences(); + + void zoomIn(); + void zoomOut(); + void zoomToFit(); + void zoomDefault(); + void scrollLeft(); + void scrollRight(); + void jumpLeft(); + void jumpRight(); + + void showNoOverlays(); + void showBasicOverlays(); + void showAllOverlays(); + + void play(); + void ffwd(); + void ffwdEnd(); + void rewind(); + void rewindStart(); + void stop(); + + void addPane(); + void addLayer(); + void deleteCurrentPane(); + void renameCurrentLayer(); + void deleteCurrentLayer(); + + void playLoopToggled(); + void playSelectionToggled(); + void playSpeedChanged(int); + void sampleRateMismatch(size_t, size_t, bool); + + void outputLevelsChanged(float, float); + + void currentPaneChanged(Pane *); + void currentLayerChanged(Pane *, Layer *); + + void toolNavigateSelected(); + void toolSelectSelected(); + void toolEditSelected(); + void toolDrawSelected(); + + void selectAll(); + void selectToStart(); + void selectToEnd(); + void selectVisible(); + void clearSelection(); + void cut(); + void copy(); + void paste(); + void deleteSelected(); + void insertInstant(); + + void documentModified(); + void documentRestored(); + + void updateMenuStates(); + void updateDescriptionLabel(); + + void layerAdded(Layer *); + void layerRemoved(Layer *); + void layerAboutToBeDeleted(Layer *); + void layerInAView(Layer *, bool); + + void mainModelChanged(WaveFileModel *); + void modelAdded(Model *); + void modelAboutToBeDeleted(Model *); + + void modelGenerationFailed(QString); + void modelRegenerationFailed(QString, QString); + + void rightButtonMenuRequested(Pane *, QPoint point); + + void preferenceChanged(PropertyContainer::PropertyName); + + void setupRecentFilesMenu(); + + void showLayerTree(); + + void website(); + void help(); + void about(); + +protected: + QString m_sessionFile; + QString m_audioFile; + Document *m_document; + + QLabel *m_descriptionLabel; + PaneStack *m_paneStack; + ViewManager *m_viewManager; + Panner *m_panner; + Fader *m_fader; + AudioDial *m_playSpeed; + WaveformLayer *m_panLayer; + Layer *m_timeRulerLayer; + + AudioCallbackPlaySource *m_playSource; + AudioCallbackPlayTarget *m_playTarget; + + bool m_mainMenusCreated; + QMenu *m_paneMenu; + QMenu *m_layerMenu; + QMenu *m_existingLayersMenu; + QMenu *m_recentFilesMenu; + QMenu *m_rightButtonMenu; + QMenu *m_rightButtonLayerMenu; + + bool m_documentModified; + + QPointer m_preferencesDialog; + + WaveFileModel *getMainModel(); + void createDocument(); + + struct PaneConfiguration { + PaneConfiguration(LayerFactory::LayerType _layer + = LayerFactory::TimeRuler, + int _channel = -1) : + layer(_layer), channel(_channel) { } + LayerFactory::LayerType layer; + int channel; + }; + + typedef std::map PaneActionMap; + PaneActionMap m_paneActions; + + typedef std::map TransformActionMap; + TransformActionMap m_layerTransformActions; + + typedef std::map LayerActionMap; + LayerActionMap m_layerActions; + + typedef std::map ExistingLayerActionMap; + ExistingLayerActionMap m_existingLayerActions; + + typedef std::map ToolActionMap; + ToolActionMap m_toolActions; + + void setupMenus(); + void setupExistingLayersMenu(); + void setupToolbars(); + Pane *addPaneToStack(); + + class PaneCallback : public SVFileReaderPaneCallback + { + public: + PaneCallback(MainWindow *mw) : m_mw(mw) { } + virtual Pane *addPane() { return m_mw->addPaneToStack(); } + virtual void setWindowSize(int width, int height) { + m_mw->resize(width, height); + } + virtual void addSelection(int start, int end) { + m_mw->m_viewManager->addSelection(Selection(start, end)); + } + protected: + MainWindow *m_mw; + }; + + class AddPaneCommand : public Command + { + public: + AddPaneCommand(MainWindow *mw); + virtual ~AddPaneCommand(); + + virtual void execute(); + virtual void unexecute(); + virtual QString getName() const; + + Pane *getPane() { return m_pane; } + + protected: + MainWindow *m_mw; + Pane *m_pane; // Main window owns this, but I determine its lifespan + Pane *m_prevCurrentPane; // I don't own this + bool m_added; + }; + + class RemovePaneCommand : public Command + { + public: + RemovePaneCommand(MainWindow *mw, Pane *pane); + virtual ~RemovePaneCommand(); + + virtual void execute(); + virtual void unexecute(); + virtual QString getName() const; + + protected: + MainWindow *m_mw; + Pane *m_pane; // Main window owns this, but I determine its lifespan + Pane *m_prevCurrentPane; // I don't own this + bool m_added; + }; + + virtual void closeEvent(QCloseEvent *e); + bool checkSaveModified(); + + void createPlayTarget(); + + void openHelpUrl(QString url); + + void toXml(QTextStream &stream); +}; + + +#endif diff -r 000000000000 -r cd5d7ff8ef38 main/PreferencesDialog.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main/PreferencesDialog.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,376 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "PreferencesDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "base/Preferences.h" +#include "fileio/ConfigFile.h" + +PreferencesDialog::PreferencesDialog(QWidget *parent, Qt::WFlags flags) : + QDialog(parent, flags) +{ + setWindowTitle(tr("Application Preferences")); + + Preferences *prefs = Preferences::getInstance(); + + QGridLayout *grid = new QGridLayout; + setLayout(grid); + + QGroupBox *groupBox = new QGroupBox; + groupBox->setTitle(tr("Sonic Visualiser Application Preferences")); + grid->addWidget(groupBox, 0, 0); + + QGridLayout *subgrid = new QGridLayout; + groupBox->setLayout(subgrid); + + // Create this first, as slots that get called from the ctor will + // refer to it + m_applyButton = new QPushButton(tr("Apply")); + + // The WindowType enum is in rather a ragbag order -- reorder it here + // in a more sensible order + m_windows = new WindowType[9]; + m_windows[0] = HanningWindow; + m_windows[1] = HammingWindow; + m_windows[2] = BlackmanWindow; + m_windows[3] = BlackmanHarrisWindow; + m_windows[4] = NuttallWindow; + m_windows[5] = GaussianWindow; + m_windows[6] = ParzenWindow; + m_windows[7] = BartlettWindow; + m_windows[8] = RectangularWindow; + + QComboBox *windowCombo = new QComboBox; + int min, max, i; + int window = prefs->getPropertyRangeAndValue("Window Type", &min, &max); + m_windowType = window; + int index = 0; + + for (i = 0; i <= 8; ++i) { + windowCombo->addItem(prefs->getPropertyValueLabel("Window Type", + m_windows[i])); + if (m_windows[i] == window) index = i; + } + + windowCombo->setCurrentIndex(index); + + m_windowTimeExampleLabel = new QLabel; + m_windowFreqExampleLabel = new QLabel; + + connect(windowCombo, SIGNAL(currentIndexChanged(int)), + this, SLOT(windowTypeChanged(int))); + windowTypeChanged(index); + + QCheckBox *smoothing = new QCheckBox; + m_smoothSpectrogram = prefs->getSmoothSpectrogram(); + smoothing->setCheckState(m_smoothSpectrogram ? + Qt::Checked : Qt::Unchecked); + + connect(smoothing, SIGNAL(stateChanged(int)), + this, SLOT(smoothSpectrogramChanged(int))); + + QComboBox *propertyLayout = new QComboBox; + int pl = prefs->getPropertyRangeAndValue("Property Box Layout", &min, &max); + m_propertyLayout = pl; + + for (i = min; i <= max; ++i) { + propertyLayout->addItem(prefs->getPropertyValueLabel("Property Box Layout", i)); + } + + propertyLayout->setCurrentIndex(pl); + + connect(propertyLayout, SIGNAL(currentIndexChanged(int)), + this, SLOT(propertyLayoutChanged(int))); + + m_tuningFrequency = prefs->getTuningFrequency(); + + QDoubleSpinBox *frequency = new QDoubleSpinBox; + frequency->setMinimum(100.0); + frequency->setMaximum(5000.0); + frequency->setSuffix(" Hz"); + frequency->setSingleStep(1); + frequency->setValue(m_tuningFrequency); + frequency->setDecimals(2); + + connect(frequency, SIGNAL(valueChanged(double)), + this, SLOT(tuningFrequencyChanged(double))); + + int row = 0; + + subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel + ("Property Box Layout"))), + row, 0); + subgrid->addWidget(propertyLayout, row++, 1, 1, 2); + + subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel + ("Tuning Frequency"))), + row, 0); + subgrid->addWidget(frequency, row++, 1, 1, 2); + + subgrid->addWidget(new QLabel(prefs->getPropertyLabel + ("Smooth Spectrogram")), + row, 0, 1, 2); + subgrid->addWidget(smoothing, row++, 2); + + subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel + ("Window Type"))), + row, 0); + subgrid->addWidget(windowCombo, row++, 1, 1, 2); + + subgrid->addWidget(m_windowTimeExampleLabel, row, 1); + subgrid->addWidget(m_windowFreqExampleLabel, row, 2); + + QHBoxLayout *hbox = new QHBoxLayout; + grid->addLayout(hbox, 1, 0); + + QPushButton *ok = new QPushButton(tr("OK")); + QPushButton *cancel = new QPushButton(tr("Cancel")); + hbox->addStretch(10); + hbox->addWidget(ok); + hbox->addWidget(m_applyButton); + hbox->addWidget(cancel); + connect(ok, SIGNAL(clicked()), this, SLOT(okClicked())); + connect(m_applyButton, SIGNAL(clicked()), this, SLOT(applyClicked())); + connect(cancel, SIGNAL(clicked()), this, SLOT(cancelClicked())); + + m_applyButton->setEnabled(false); +} + +PreferencesDialog::~PreferencesDialog() +{ + std::cerr << "PreferencesDialog::~PreferencesDialog()" << std::endl; + + delete[] m_windows; +} + +void +PreferencesDialog::windowTypeChanged(int value) +{ + int step = 24; + int peak = 48; + int w = step * 4, h = 64; + WindowType type = m_windows[value]; + Window windower = Window(type, step * 2); + + QPixmap timeLabel(w, h + 1); + timeLabel.fill(Qt::white); + QPainter timePainter(&timeLabel); + + QPainterPath path; + + path.moveTo(0, h - peak + 1); + path.lineTo(w, h - peak + 1); + + timePainter.setPen(Qt::gray); + timePainter.setRenderHint(QPainter::Antialiasing, true); + timePainter.drawPath(path); + + path = QPainterPath(); + + float acc[w]; + for (int i = 0; i < w; ++i) acc[i] = 0.f; + for (int j = 0; j < 3; ++j) { + for (int i = 0; i < step * 2; ++i) { + acc[j * step + i] += windower.getValue(i); + } + } + for (int i = 0; i < w; ++i) { + int y = h - int(peak * acc[i] + 0.001) + 1; + if (i == 0) path.moveTo(i, y); + else path.lineTo(i, y); + } + + timePainter.drawPath(path); + timePainter.setRenderHint(QPainter::Antialiasing, false); + + path = QPainterPath(); + + timePainter.setPen(Qt::black); + + for (int i = 0; i < step * 2; ++i) { + int y = h - int(peak * windower.getValue(i) + 0.001) + 1; + if (i == 0) path.moveTo(i + step, float(y)); + else path.lineTo(i + step, float(y)); + } + + if (type == RectangularWindow) { + timePainter.drawPath(path); + path = QPainterPath(); + } + + timePainter.setRenderHint(QPainter::Antialiasing, true); + path.addRect(0, 0, w, h + 1); + timePainter.drawPath(path); + + QFont font; + font.setPixelSize(10); + font.setItalic(true); + timePainter.setFont(font); + QString label = tr("V / time"); + timePainter.drawText(w - timePainter.fontMetrics().width(label) - 4, + timePainter.fontMetrics().ascent() + 1, label); + + m_windowTimeExampleLabel->setPixmap(timeLabel); + + int fw = 100; + + QPixmap freqLabel(fw, h + 1); + freqLabel.fill(Qt::white); + QPainter freqPainter(&freqLabel); + path = QPainterPath(); + + size_t fftsize = 512; + + float *input = (float *)fftwf_malloc(fftsize * sizeof(float)); + fftwf_complex *output = + (fftwf_complex *)fftwf_malloc(fftsize * sizeof(fftwf_complex)); + fftwf_plan plan = fftwf_plan_dft_r2c_1d(fftsize, input, output, + FFTW_ESTIMATE); + for (int i = 0; i < fftsize; ++i) input[i] = 0.f; + for (int i = 0; i < step * 2; ++i) { + input[fftsize/2 - step + i] = windower.getValue(i); + } + + fftwf_execute(plan); + fftwf_destroy_plan(plan); + + float maxdb = 0.f; + float mindb = 0.f; + bool first = true; + for (int i = 0; i < fftsize/2; ++i) { + float power = output[i][0] * output[i][0] + output[i][1] * output[i][1]; + float db = mindb; + if (power > 0) { + db = 20 * log10(power); + if (first || db > maxdb) maxdb = db; + if (first || db < mindb) mindb = db; + first = false; + } + } + + if (mindb > -80.f) mindb = -80.f; + + // -- no, don't use the actual mindb -- it's easier to compare + // plots with a fixed min value + mindb = -170.f; + + float maxval = maxdb + -mindb; + + float ly = h - ((-80.f + -mindb) / maxval) * peak + 1; + + path.moveTo(0, h - peak + 1); + path.lineTo(fw, h - peak + 1); + + freqPainter.setPen(Qt::gray); + freqPainter.setRenderHint(QPainter::Antialiasing, true); + freqPainter.drawPath(path); + + path = QPainterPath(); + freqPainter.setPen(Qt::black); + +// std::cerr << "maxdb = " << maxdb << ", mindb = " << mindb << ", maxval = " <setPixmap(freqLabel); + + m_windowType = type; + m_applyButton->setEnabled(true); +} + +void +PreferencesDialog::smoothSpectrogramChanged(int state) +{ + m_smoothSpectrogram = (state == Qt::Checked); + m_applyButton->setEnabled(true); +} + +void +PreferencesDialog::propertyLayoutChanged(int layout) +{ + m_propertyLayout = layout; + m_applyButton->setEnabled(true); +} + +void +PreferencesDialog::tuningFrequencyChanged(double freq) +{ + m_tuningFrequency = freq; + m_applyButton->setEnabled(true); +} + +void +PreferencesDialog::okClicked() +{ + applyClicked(); + Preferences::getInstance()->getConfigFile()->commit(); + accept(); +} + +void +PreferencesDialog::applyClicked() +{ + Preferences *prefs = Preferences::getInstance(); + prefs->setWindowType(WindowType(m_windowType)); + prefs->setSmoothSpectrogram(m_smoothSpectrogram); + prefs->setPropertyBoxLayout(Preferences::PropertyBoxLayout + (m_propertyLayout)); + prefs->setTuningFrequency(m_tuningFrequency); + m_applyButton->setEnabled(false); +} + +void +PreferencesDialog::cancelClicked() +{ + reject(); +} + diff -r 000000000000 -r cd5d7ff8ef38 main/PreferencesDialog.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main/PreferencesDialog.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,58 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _PREFERENCES_DIALOG_H_ +#define _PREFERENCES_DIALOG_H_ + +#include + +#include "base/Window.h" + +class QLabel; +class QPushButton; + +class PreferencesDialog : public QDialog +{ + Q_OBJECT + +public: + PreferencesDialog(QWidget *parent = 0, Qt::WFlags flags = 0); + ~PreferencesDialog(); + +protected slots: + void windowTypeChanged(int type); + void smoothSpectrogramChanged(int state); + void propertyLayoutChanged(int layout); + void tuningFrequencyChanged(double freq); + + void okClicked(); + void applyClicked(); + void cancelClicked(); + +protected: + QLabel *m_windowTimeExampleLabel; + QLabel *m_windowFreqExampleLabel; + + WindowType *m_windows; + + QPushButton *m_applyButton; + + int m_windowType; + bool m_smoothSpectrogram; + int m_propertyLayout; + float m_tuningFrequency; +}; + +#endif diff -r 000000000000 -r cd5d7ff8ef38 main/main.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main/main.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,120 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "MainWindow.h" + +#include "base/System.h" +#include "base/TempDirectory.h" +#include "base/PropertyContainer.h" +#include "base/Preferences.h" +#include "fileio/ConfigFile.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +//!!! catch trappable signals, cleanup temporary directory etc +//!!! check for crap left over from previous run + +static QMutex cleanupMutex; + +static void +signalHandler(int /* signal */) +{ + // Avoid this happening more than once across threads + + cleanupMutex.lock(); + std::cerr << "signalHandler: cleaning up and exiting" << std::endl; + TempDirectory::getInstance()->cleanup(); + exit(0); // without releasing mutex +} + +extern void svSystemSpecificInitialisation(); + +int +main(int argc, char **argv) +{ + QApplication application(argc, argv); + + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + +#ifndef Q_WS_WIN32 + signal(SIGHUP, signalHandler); + signal(SIGQUIT, signalHandler); +#endif + + svSystemSpecificInitialisation(); + + QString language = QLocale::system().name(); + + QTranslator qtTranslator; + QString qtTrName = QString("qt_%1").arg(language); + std::cerr << "Loading " << qtTrName.toStdString() << "..." << std::endl; + qtTranslator.load(qtTrName); + application.installTranslator(&qtTranslator); + + QTranslator svTranslator; + QString svTrName = QString("sonic-visualiser_%1").arg(language); + std::cerr << "Loading " << svTrName.toStdString() << "..." << std::endl; + svTranslator.load(svTrName, ":i18n"); + application.installTranslator(&svTranslator); + + // Permit size_t and PropertyName to be used as args in queued signal calls + qRegisterMetaType("size_t"); + qRegisterMetaType("PropertyContainer::PropertyName"); + + MainWindow gui; + + QDesktopWidget *desktop = QApplication::desktop(); + QRect available = desktop->availableGeometry(); + + int width = available.width() * 2 / 3; + int height = available.height() / 2; + if (height < 450) height = available.height() * 2 / 3; + if (width > height * 2) width = height * 2; + + gui.resize(width, height); + gui.show(); + + if (argc > 1) { + QString path = argv[1]; + bool success = false; + if (path.endsWith(".sv")) { + success = gui.openSessionFile(path); + } + if (!success) { + success = gui.openSomeFile(path); + } + if (!success) { + QMessageBox::critical(&gui, QMessageBox::tr("Failed to open file"), + QMessageBox::tr("File \"%1\" could not be opened").arg(path)); + } + } + + int rv = application.exec(); + std::cerr << "application.exec() returned " << rv << std::endl; + + cleanupMutex.lock(); + TempDirectory::getInstance()->cleanup(); + Preferences::getInstance()->getConfigFile()->commit(); + return rv; +} diff -r 000000000000 -r cd5d7ff8ef38 main/systeminit.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/main/systeminit.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,92 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include +#include + +#include + +#ifdef Q_WS_X11 +#include +#include +#include +#include + +static int handle_x11_error(Display *dpy, XErrorEvent *err) +{ + char errstr[256]; + XGetErrorText(dpy, err->error_code, errstr, 256); + if (err->error_code != BadWindow) { + std::cerr << "waveform: X Error: " + << errstr << " " << int(err->error_code) + << "\nin major opcode: " + << int(err->request_code) << std::endl; + } + return 0; +} +#endif + +#ifdef Q_WS_WIN32 + +#include + +// Set default file open mode to binary +#undef _fmode +int _fmode = _O_BINARY; + +void redirectStderr() +{ + HANDLE stderrHandle = GetStdHandle(STD_ERROR_HANDLE); + if (!stderrHandle) return; + + AllocConsole(); + + CONSOLE_SCREEN_BUFFER_INFO info; + GetConsoleScreenBufferInfo(stderrHandle, &info); + info.dwSize.Y = 1000; + SetConsoleScreenBufferSize(stderrHandle, info.dwSize); + + int h = _open_osfhandle((long)stderrHandle, _O_TEXT); + if (h) { + FILE *fd = _fdopen(h, "w"); + if (fd) { + *stderr = *fd; + setvbuf(stderr, NULL, _IONBF, 0); + } + } +} + +#endif + +extern void svSystemSpecificInitialisation() +{ +#ifdef Q_WS_X11 + XSetErrorHandler(handle_x11_error); +#endif + +#ifdef Q_WS_WIN32 + redirectStderr(); + QFont fn = qApp->font(); + fn.setFamily("Tahoma"); + qApp->setFont(fn); +#else +#ifdef Q_WS_X11 + QFont fn = qApp->font(); + fn.setPointSize(fn.pointSize() + 2); + qApp->setFont(fn); +#endif +#endif +} + diff -r 000000000000 -r cd5d7ff8ef38 transform/FeatureExtractionPluginTransform.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/transform/FeatureExtractionPluginTransform.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,494 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "FeatureExtractionPluginTransform.h" + +#include "plugin/FeatureExtractionPluginFactory.h" +#include "plugin/PluginXml.h" +#include "vamp-sdk/Plugin.h" + +#include "base/Model.h" +#include "base/Window.h" +#include "model/SparseOneDimensionalModel.h" +#include "model/SparseTimeValueModel.h" +#include "model/DenseThreeDimensionalModel.h" +#include "model/DenseTimeValueModel.h" +#include "model/NoteModel.h" +#include "fileio/FFTFuzzyAdapter.h" + +#include + +#include + +FeatureExtractionPluginTransform::FeatureExtractionPluginTransform(Model *inputModel, + QString pluginId, + int channel, + QString configurationXml, + QString outputName) : + Transform(inputModel), + m_plugin(0), + m_channel(channel), + m_stepSize(0), + m_blockSize(0), + m_descriptor(0), + m_outputFeatureNo(0) +{ +// std::cerr << "FeatureExtractionPluginTransform::FeatureExtractionPluginTransform: plugin " << pluginId.toStdString() << ", outputName " << outputName.toStdString() << std::endl; + + FeatureExtractionPluginFactory *factory = + FeatureExtractionPluginFactory::instanceFor(pluginId); + + if (!factory) { + std::cerr << "FeatureExtractionPluginTransform: No factory available for plugin id \"" + << pluginId.toStdString() << "\"" << std::endl; + return; + } + + m_plugin = factory->instantiatePlugin(pluginId, m_input->getSampleRate()); + + if (!m_plugin) { + std::cerr << "FeatureExtractionPluginTransform: Failed to instantiate plugin \"" + << pluginId.toStdString() << "\"" << std::endl; + return; + } + + if (configurationXml != "") { + PluginXml(m_plugin).setParametersFromXml(configurationXml); + } + + m_blockSize = m_plugin->getPreferredBlockSize(); + m_stepSize = m_plugin->getPreferredStepSize(); + + if (m_blockSize == 0) m_blockSize = 1024; //!!! todo: ask user + if (m_stepSize == 0) m_stepSize = m_blockSize; //!!! likewise + + DenseTimeValueModel *input = getInput(); + if (!input) return; + + size_t channelCount = input->getChannelCount(); + if (m_plugin->getMaxChannelCount() < channelCount) { + channelCount = 1; + } + if (m_plugin->getMinChannelCount() > channelCount) { + std::cerr << "FeatureExtractionPluginTransform:: " + << "Can't provide enough channels to plugin (plugin min " + << m_plugin->getMinChannelCount() << ", max " + << m_plugin->getMaxChannelCount() << ", input model has " + << input->getChannelCount() << ")" << std::endl; + return; + } + + if (!m_plugin->initialise(channelCount, m_stepSize, m_blockSize)) { + std::cerr << "FeatureExtractionPluginTransform: Plugin " + << m_plugin->getName() << " failed to initialise!" << std::endl; + return; + } + + Vamp::Plugin::OutputList outputs = m_plugin->getOutputDescriptors(); + + if (outputs.empty()) { + std::cerr << "FeatureExtractionPluginTransform: Plugin \"" + << pluginId.toStdString() << "\" has no outputs" << std::endl; + return; + } + + for (size_t i = 0; i < outputs.size(); ++i) { + if (outputName == "" || outputs[i].name == outputName.toStdString()) { + m_outputFeatureNo = i; + m_descriptor = new Vamp::Plugin::OutputDescriptor + (outputs[i]); + break; + } + } + + if (!m_descriptor) { + std::cerr << "FeatureExtractionPluginTransform: Plugin \"" + << pluginId.toStdString() << "\" has no output named \"" + << outputName.toStdString() << "\"" << std::endl; + return; + } + +// std::cerr << "FeatureExtractionPluginTransform: output sample type " +// << m_descriptor->sampleType << std::endl; + + int binCount = 1; + float minValue = 0.0, maxValue = 0.0; + + if (m_descriptor->hasFixedBinCount) { + binCount = m_descriptor->binCount; + } + +// std::cerr << "FeatureExtractionPluginTransform: output bin count " +// << binCount << std::endl; + + if (binCount > 0 && m_descriptor->hasKnownExtents) { + minValue = m_descriptor->minValue; + maxValue = m_descriptor->maxValue; + } + + size_t modelRate = m_input->getSampleRate(); + size_t modelResolution = 1; + + switch (m_descriptor->sampleType) { + + case Vamp::Plugin::OutputDescriptor::VariableSampleRate: + if (m_descriptor->sampleRate != 0.0) { + modelResolution = size_t(modelRate / m_descriptor->sampleRate + 0.001); + } + break; + + case Vamp::Plugin::OutputDescriptor::OneSamplePerStep: + modelResolution = m_stepSize; + break; + + case Vamp::Plugin::OutputDescriptor::FixedSampleRate: + modelRate = size_t(m_descriptor->sampleRate + 0.001); + break; + } + + if (binCount == 0) { + + m_output = new SparseOneDimensionalModel(modelRate, modelResolution, + false); + + } else if (binCount == 1) { + + SparseTimeValueModel *model = new SparseTimeValueModel + (modelRate, modelResolution, minValue, maxValue, false); + model->setScaleUnits(outputs[m_outputFeatureNo].unit.c_str()); + + m_output = model; + + } else if (m_descriptor->sampleType == + Vamp::Plugin::OutputDescriptor::VariableSampleRate) { + + // We don't have a sparse 3D model, so interpret this as a + // note model. There's nothing to define which values to use + // as which parameters of the note -- for the moment let's + // treat the first as pitch, second as duration in frames, + // third (if present) as velocity. (Our note model doesn't + // yet store velocity.) + //!!! todo: ask the user! + + NoteModel *model = new NoteModel + (modelRate, modelResolution, minValue, maxValue, false); + model->setScaleUnits(outputs[m_outputFeatureNo].unit.c_str()); + + m_output = model; + + } else { + + m_output = new DenseThreeDimensionalModel(modelRate, modelResolution, + binCount, false); + + if (!m_descriptor->binNames.empty()) { + std::vector names; + for (size_t i = 0; i < m_descriptor->binNames.size(); ++i) { + names.push_back(m_descriptor->binNames[i].c_str()); + } + (dynamic_cast(m_output)) + ->setBinNames(names); + } + } +} + +FeatureExtractionPluginTransform::~FeatureExtractionPluginTransform() +{ + delete m_plugin; + delete m_descriptor; +} + +DenseTimeValueModel * +FeatureExtractionPluginTransform::getInput() +{ + DenseTimeValueModel *dtvm = + dynamic_cast(getInputModel()); + if (!dtvm) { + std::cerr << "FeatureExtractionPluginTransform::getInput: WARNING: Input model is not conformable to DenseTimeValueModel" << std::endl; + } + return dtvm; +} + +void +FeatureExtractionPluginTransform::run() +{ + DenseTimeValueModel *input = getInput(); + if (!input) return; + + if (!m_output) return; + + size_t sampleRate = m_input->getSampleRate(); + + size_t channelCount = input->getChannelCount(); + if (m_plugin->getMaxChannelCount() < channelCount) { + channelCount = 1; + } + + float **buffers = new float*[channelCount]; + for (size_t ch = 0; ch < channelCount; ++ch) { + buffers[ch] = new float[m_blockSize]; + } + + bool frequencyDomain = (m_plugin->getInputDomain() == + Vamp::Plugin::FrequencyDomain); + std::vector fftAdapters; + + if (frequencyDomain) { + for (size_t ch = 0; ch < channelCount; ++ch) { + fftAdapters.push_back(new FFTFuzzyAdapter + (getInput(), + channelCount == 1 ? m_channel : ch, + HanningWindow, + m_blockSize, + m_stepSize, + m_blockSize, + false)); + } + } + + long startFrame = m_input->getStartFrame(); + long endFrame = m_input->getEndFrame(); + long blockFrame = startFrame; + + long prevCompletion = 0; + + while (1) { + + if (frequencyDomain) { + if (blockFrame - int(m_blockSize)/2 > endFrame) break; + } else { + if (blockFrame >= endFrame) break; + } + +// std::cerr << "FeatureExtractionPluginTransform::run: blockFrame " +// << blockFrame << std::endl; + + long completion = + (((blockFrame - startFrame) / m_stepSize) * 99) / + ( (endFrame - startFrame) / m_stepSize); + + // channelCount is either m_input->channelCount or 1 + + for (size_t ch = 0; ch < channelCount; ++ch) { + if (frequencyDomain) { + int column = (blockFrame - startFrame) / m_stepSize; + for (size_t i = 0; i < m_blockSize/2; ++i) { + fftAdapters[ch]->getValuesAt + (column, i, buffers[ch][i*2], buffers[ch][i*2+1]); + } +/*!!! + float sum = 0.0; + for (size_t i = 0; i < m_blockSize/2; ++i) { + sum += buffers[ch][i*2]; + } + if (fabs(sum) < 0.0001) { + std::cerr << "WARNING: small sum for column " << column << " (sum is " << sum << ")" << std::endl; + } +*/ + } else { + getFrames(ch, channelCount, + blockFrame, m_blockSize, buffers[ch]); + } + } + + Vamp::Plugin::FeatureSet features = m_plugin->process + (buffers, Vamp::RealTime::frame2RealTime(blockFrame, sampleRate)); + + for (size_t fi = 0; fi < features[m_outputFeatureNo].size(); ++fi) { + Vamp::Plugin::Feature feature = + features[m_outputFeatureNo][fi]; + addFeature(blockFrame, feature); + } + + if (blockFrame == startFrame || completion > prevCompletion) { + setCompletion(completion); + prevCompletion = completion; + } + + blockFrame += m_stepSize; + } + + Vamp::Plugin::FeatureSet features = m_plugin->getRemainingFeatures(); + + for (size_t fi = 0; fi < features[m_outputFeatureNo].size(); ++fi) { + Vamp::Plugin::Feature feature = + features[m_outputFeatureNo][fi]; + addFeature(blockFrame, feature); + } + + if (frequencyDomain) { + for (size_t ch = 0; ch < channelCount; ++ch) { + delete fftAdapters[ch]; + } + } + + setCompletion(100); +} + +void +FeatureExtractionPluginTransform::getFrames(int channel, int channelCount, + long startFrame, long size, + float *buffer) +{ + long offset = 0; + + if (startFrame < 0) { + for (int i = 0; i < size && startFrame + i < 0; ++i) { + buffer[i] = 0.0f; + } + offset = -startFrame; + size -= offset; + if (size <= 0) return; + startFrame = 0; + } + + long got = getInput()->getValues + ((channelCount == 1 ? m_channel : channel), + startFrame, startFrame + size, buffer + offset); + + while (got < size) { + buffer[offset + got] = 0.0; + ++got; + } + + if (m_channel == -1 && channelCount == 1 && + getInput()->getChannelCount() > 1) { + // use mean instead of sum, as plugin input + int cc = getInput()->getChannelCount(); + for (long i = 0; i < size; ++i) { + buffer[i] /= cc; + } + } +} + +void +FeatureExtractionPluginTransform::addFeature(size_t blockFrame, + const Vamp::Plugin::Feature &feature) +{ + size_t inputRate = m_input->getSampleRate(); + +// std::cerr << "FeatureExtractionPluginTransform::addFeature(" +// << blockFrame << ")" << std::endl; + + int binCount = 1; + if (m_descriptor->hasFixedBinCount) { + binCount = m_descriptor->binCount; + } + + size_t frame = blockFrame; + + if (m_descriptor->sampleType == + Vamp::Plugin::OutputDescriptor::VariableSampleRate) { + + if (!feature.hasTimestamp) { + std::cerr + << "WARNING: FeatureExtractionPluginTransform::addFeature: " + << "Feature has variable sample rate but no timestamp!" + << std::endl; + return; + } else { + frame = Vamp::RealTime::realTime2Frame(feature.timestamp, inputRate); + } + + } else if (m_descriptor->sampleType == + Vamp::Plugin::OutputDescriptor::FixedSampleRate) { + + if (feature.hasTimestamp) { + //!!! warning: sampleRate may be non-integral + frame = Vamp::RealTime::realTime2Frame(feature.timestamp, + m_descriptor->sampleRate); + } else { + frame = m_output->getEndFrame() + 1; + } + } + + if (binCount == 0) { + + SparseOneDimensionalModel *model = getOutput(); + if (!model) return; + model->addPoint(SparseOneDimensionalModel::Point(frame, feature.label.c_str())); + + } else if (binCount == 1) { + + float value = 0.0; + if (feature.values.size() > 0) value = feature.values[0]; + + SparseTimeValueModel *model = getOutput(); + if (!model) return; + model->addPoint(SparseTimeValueModel::Point(frame, value, feature.label.c_str())); + + } else if (m_descriptor->sampleType == + Vamp::Plugin::OutputDescriptor::VariableSampleRate) { + + float pitch = 0.0; + if (feature.values.size() > 0) pitch = feature.values[0]; + + float duration = 1; + if (feature.values.size() > 1) duration = feature.values[1]; + + float velocity = 100; + if (feature.values.size() > 2) velocity = feature.values[2]; + + NoteModel *model = getOutput(); + if (!model) return; + + model->addPoint(NoteModel::Point(frame, pitch, duration, feature.label.c_str())); + + } else { + + DenseThreeDimensionalModel::BinValueSet values = feature.values; + + DenseThreeDimensionalModel *model = getOutput(); + if (!model) return; + + model->setBinValues(frame, values); + } +} + +void +FeatureExtractionPluginTransform::setCompletion(int completion) +{ + int binCount = 1; + if (m_descriptor->hasFixedBinCount) { + binCount = m_descriptor->binCount; + } + + if (binCount == 0) { + + SparseOneDimensionalModel *model = getOutput(); + if (!model) return; + model->setCompletion(completion); + + } else if (binCount == 1) { + + SparseTimeValueModel *model = getOutput(); + if (!model) return; + model->setCompletion(completion); + + } else if (m_descriptor->sampleType == + Vamp::Plugin::OutputDescriptor::VariableSampleRate) { + + NoteModel *model = getOutput(); + if (!model) return; + model->setCompletion(completion); + + } else { + + DenseThreeDimensionalModel *model = getOutput(); + if (!model) return; + model->setCompletion(completion); + } +} + diff -r 000000000000 -r cd5d7ff8ef38 transform/FeatureExtractionPluginTransform.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/transform/FeatureExtractionPluginTransform.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,65 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _FEATURE_EXTRACTION_PLUGIN_TRANSFORM_H_ +#define _FEATURE_EXTRACTION_PLUGIN_TRANSFORM_H_ + +#include "Transform.h" + +#include "vamp-sdk/Plugin.h" + +class DenseTimeValueModel; + +class FeatureExtractionPluginTransform : public Transform +{ +public: + FeatureExtractionPluginTransform(Model *inputModel, + QString plugin, + int channel, + QString configurationXml = "", + QString outputName = ""); + virtual ~FeatureExtractionPluginTransform(); + +protected: + virtual void run(); + + Vamp::Plugin *m_plugin; + int m_channel; + size_t m_stepSize; + size_t m_blockSize; + Vamp::Plugin::OutputDescriptor *m_descriptor; + int m_outputFeatureNo; + + void addFeature(size_t blockFrame, + const Vamp::Plugin::Feature &feature); + + void setCompletion(int); + + void getFrames(int channel, int channelCount, + long startFrame, long size, float *buffer); + + // just casts + DenseTimeValueModel *getInput(); + template ModelClass *getOutput() { + ModelClass *mc = dynamic_cast(m_output); + if (!mc) { + std::cerr << "FeatureExtractionPluginTransform::getOutput: Output model not conformable" << std::endl; + } + return mc; + } +}; + +#endif + diff -r 000000000000 -r cd5d7ff8ef38 transform/RealTimePluginTransform.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/transform/RealTimePluginTransform.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,172 @@ + +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "RealTimePluginTransform.h" + +#include "plugin/RealTimePluginFactory.h" +#include "plugin/RealTimePluginInstance.h" +#include "plugin/PluginXml.h" + +#include "base/Model.h" +#include "model/SparseTimeValueModel.h" +#include "model/DenseTimeValueModel.h" + +#include + +RealTimePluginTransform::RealTimePluginTransform(Model *inputModel, + QString pluginId, + int channel, + QString configurationXml, + QString units, + int output) : + Transform(inputModel), + m_plugin(0), + m_channel(channel), + m_outputNo(output) +{ + std::cerr << "RealTimePluginTransform::RealTimePluginTransform: plugin " << pluginId.toStdString() << ", output " << output << std::endl; + + RealTimePluginFactory *factory = + RealTimePluginFactory::instanceFor(pluginId); + + if (!factory) { + std::cerr << "RealTimePluginTransform: No factory available for plugin id \"" + << pluginId.toStdString() << "\"" << std::endl; + return; + } + + DenseTimeValueModel *input = getInput(); + if (!input) return; + + m_plugin = factory->instantiatePlugin(pluginId, 0, 0, m_input->getSampleRate(), + 1024, //!!! wants to be configurable + input->getChannelCount()); + + if (!m_plugin) { + std::cerr << "RealTimePluginTransform: Failed to instantiate plugin \"" + << pluginId.toStdString() << "\"" << std::endl; + return; + } + + if (configurationXml != "") { + PluginXml(m_plugin).setParametersFromXml(configurationXml); + } + + if (m_outputNo >= m_plugin->getControlOutputCount()) { + std::cerr << "RealTimePluginTransform: Plugin has fewer than desired " << m_outputNo << " control outputs" << std::endl; + return; + } + + SparseTimeValueModel *model = new SparseTimeValueModel + (input->getSampleRate(), 1024, //!!! + 0.0, 0.0, false); + + if (units != "") model->setScaleUnits(units); + + m_output = model; +} + +RealTimePluginTransform::~RealTimePluginTransform() +{ + delete m_plugin; +} + +DenseTimeValueModel * +RealTimePluginTransform::getInput() +{ + DenseTimeValueModel *dtvm = + dynamic_cast(getInputModel()); + if (!dtvm) { + std::cerr << "RealTimePluginTransform::getInput: WARNING: Input model is not conformable to DenseTimeValueModel" << std::endl; + } + return dtvm; +} + +void +RealTimePluginTransform::run() +{ + DenseTimeValueModel *input = getInput(); + if (!input) return; + + SparseTimeValueModel *model = dynamic_cast(m_output); + if (!model) return; + + if (m_outputNo >= m_plugin->getControlOutputCount()) return; + + size_t sampleRate = input->getSampleRate(); + int channelCount = input->getChannelCount(); + if (m_channel != -1) channelCount = 1; + + size_t blockSize = m_plugin->getBufferSize(); + + float **buffers = m_plugin->getAudioInputBuffers(); + + size_t startFrame = m_input->getStartFrame(); + size_t endFrame = m_input->getEndFrame(); + size_t blockFrame = startFrame; + + size_t prevCompletion = 0; + + int i = 0; + + while (blockFrame < endFrame) { + + size_t completion = + (((blockFrame - startFrame) / blockSize) * 99) / + ( (endFrame - startFrame) / blockSize); + + size_t got = 0; + + if (channelCount == 1) { + got = input->getValues + (m_channel, blockFrame, blockFrame + blockSize, buffers[0]); + while (got < blockSize) { + buffers[0][got++] = 0.0; + } + if (m_channel == -1 && channelCount > 1) { + // use mean instead of sum, as plugin input + for (size_t i = 0; i < got; ++i) { + buffers[0][i] /= channelCount; + } + } + } else { + for (size_t ch = 0; ch < channelCount; ++ch) { + got = input->getValues + (ch, blockFrame, blockFrame + blockSize, buffers[ch]); + while (got < blockSize) { + buffers[ch][got++] = 0.0; + } + } + } + + m_plugin->run(Vamp::RealTime::frame2RealTime(blockFrame, sampleRate)); + + float value = m_plugin->getControlOutputValue(m_outputNo); + + model->addPoint(SparseTimeValueModel::Point + (blockFrame - m_plugin->getLatency(), value, "")); + + if (blockFrame == startFrame || completion > prevCompletion) { + model->setCompletion(completion); + prevCompletion = completion; + } + + blockFrame += blockSize; + } + + model->setCompletion(100); +} + diff -r 000000000000 -r cd5d7ff8ef38 transform/RealTimePluginTransform.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/transform/RealTimePluginTransform.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,47 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _REAL_TIME_PLUGIN_TRANSFORM_H_ +#define _REAL_TIME_PLUGIN_TRANSFORM_H_ + +#include "Transform.h" +#include "RealTimePluginInstance.h" + +class DenseTimeValueModel; + +class RealTimePluginTransform : public Transform +{ +public: + RealTimePluginTransform(Model *inputModel, + QString plugin, + int channel, + QString configurationXml = "", + QString units = "", + int output = 0); + virtual ~RealTimePluginTransform(); + +protected: + virtual void run(); + + RealTimePluginInstance *m_plugin; + int m_channel; + int m_outputNo; + + // just casts + DenseTimeValueModel *getInput(); +}; + +#endif + diff -r 000000000000 -r cd5d7ff8ef38 transform/Transform.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/transform/Transform.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,32 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "Transform.h" + +Transform::Transform(Model *m) : + m_input(m), + m_output(0), + m_detached(false), + m_deleting(false) +{ +} + +Transform::~Transform() +{ + m_deleting = true; + wait(); + if (!m_detached) delete m_output; +} + diff -r 000000000000 -r cd5d7ff8ef38 transform/Transform.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/transform/Transform.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,56 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _TRANSFORM_H_ +#define _TRANSFORM_H_ + +#include "Thread.h" + +#include "base/Model.h" + +typedef QString TransformName; + +/** + * A Transform turns one data model into another. + * + * Typically in this application, a Transform might have a + * DenseTimeValueModel as its input (e.g. an audio waveform) and a + * SparseOneDimensionalModel (e.g. detected beats) as its output. + * + * The Transform typically runs in the background, as a separate + * thread populating the output model. The model is available to the + * user of the Transform immediately, but may be initially empty until + * the background thread has populated it. + */ + +class Transform : public Thread +{ +public: + virtual ~Transform(); + + Model *getInputModel() { return m_input; } + Model *getOutputModel() { return m_output; } + Model *detachOutputModel() { m_detached = true; return m_output; } + +protected: + Transform(Model *m); + + Model *m_input; // I don't own this + Model *m_output; // I own this, unless... + bool m_detached; // ... this is true. + bool m_deleting; +}; + +#endif diff -r 000000000000 -r cd5d7ff8ef38 transform/TransformFactory.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/transform/TransformFactory.cpp Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,480 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#include "TransformFactory.h" + +#include "FeatureExtractionPluginTransform.h" +#include "RealTimePluginTransform.h" + +#include "plugin/FeatureExtractionPluginFactory.h" +#include "plugin/RealTimePluginFactory.h" +#include "plugin/PluginXml.h" + +#include "widgets/PluginParameterDialog.h" + +#include "model/DenseTimeValueModel.h" + +#include +#include + +#include + +TransformFactory * +TransformFactory::m_instance = new TransformFactory; + +TransformFactory * +TransformFactory::getInstance() +{ + return m_instance; +} + +TransformFactory::~TransformFactory() +{ +} + +TransformFactory::TransformList +TransformFactory::getAllTransforms() +{ + if (m_transforms.empty()) populateTransforms(); + + TransformList list; + for (TransformDescriptionMap::const_iterator i = m_transforms.begin(); + i != m_transforms.end(); ++i) { + list.push_back(i->second); + } + + return list; +} + +std::vector +TransformFactory::getAllTransformTypes() +{ + if (m_transforms.empty()) populateTransforms(); + + std::set types; + for (TransformDescriptionMap::const_iterator i = m_transforms.begin(); + i != m_transforms.end(); ++i) { + types.insert(i->second.type); + } + + std::vector rv; + for (std::set::iterator i = types.begin(); i != types.end(); ++i) { + rv.push_back(*i); + } + + return rv; +} + +void +TransformFactory::populateTransforms() +{ + TransformDescriptionMap transforms; + + populateFeatureExtractionPlugins(transforms); + populateRealTimePlugins(transforms); + + // disambiguate plugins with similar descriptions + + std::map descriptions; + + for (TransformDescriptionMap::iterator i = transforms.begin(); + i != transforms.end(); ++i) { + + TransformDesc desc = i->second; + + ++descriptions[desc.description]; + ++descriptions[QString("%1 [%2]").arg(desc.description).arg(desc.maker)]; + } + + std::map counts; + m_transforms.clear(); + + for (TransformDescriptionMap::iterator i = transforms.begin(); + i != transforms.end(); ++i) { + + TransformDesc desc = i->second; + QString name = desc.name; + QString description = desc.description; + QString maker = desc.maker; + + if (descriptions[description] > 1) { + description = QString("%1 [%2]").arg(description).arg(maker); + if (descriptions[description] > 1) { + description = QString("%1 <%2>") + .arg(description).arg(++counts[description]); + } + } + + desc.description = description; + m_transforms[name] = desc; + } +} + +void +TransformFactory::populateFeatureExtractionPlugins(TransformDescriptionMap &transforms) +{ + std::vector plugs = + FeatureExtractionPluginFactory::getAllPluginIdentifiers(); + + for (size_t i = 0; i < plugs.size(); ++i) { + + QString pluginId = plugs[i]; + + FeatureExtractionPluginFactory *factory = + FeatureExtractionPluginFactory::instanceFor(pluginId); + + if (!factory) { + std::cerr << "WARNING: TransformFactory::populateTransforms: No feature extraction plugin factory for instance " << pluginId.toLocal8Bit().data() << std::endl; + continue; + } + + Vamp::Plugin *plugin = + factory->instantiatePlugin(pluginId, 48000); + + if (!plugin) { + std::cerr << "WARNING: TransformFactory::populateTransforms: Failed to instantiate plugin " << pluginId.toLocal8Bit().data() << std::endl; + continue; + } + + QString pluginDescription = plugin->getDescription().c_str(); + Vamp::Plugin::OutputList outputs = + plugin->getOutputDescriptors(); + + for (size_t j = 0; j < outputs.size(); ++j) { + + QString transformName = QString("%1:%2") + .arg(pluginId).arg(outputs[j].name.c_str()); + + QString userDescription; + QString friendlyName; + QString units = outputs[j].unit.c_str(); + + if (outputs.size() == 1) { + userDescription = pluginDescription; + friendlyName = pluginDescription; + } else { + userDescription = QString("%1: %2") + .arg(pluginDescription) + .arg(outputs[j].description.c_str()); + friendlyName = outputs[j].description.c_str(); + } + + bool configurable = (!plugin->getPrograms().empty() || + !plugin->getParameterDescriptors().empty()); + + transforms[transformName] = + TransformDesc(tr("Analysis Plugins"), + transformName, + userDescription, + friendlyName, + plugin->getMaker().c_str(), + units, + configurable); + } + } +} + +void +TransformFactory::populateRealTimePlugins(TransformDescriptionMap &transforms) +{ + std::vector plugs = + RealTimePluginFactory::getAllPluginIdentifiers(); + + QRegExp unitRE("[\\[\\(]([A-Za-z0-9/]+)[\\)\\]]$"); + + for (size_t i = 0; i < plugs.size(); ++i) { + + QString pluginId = plugs[i]; + + RealTimePluginFactory *factory = + RealTimePluginFactory::instanceFor(pluginId); + + if (!factory) { + std::cerr << "WARNING: TransformFactory::populateTransforms: No real time plugin factory for instance " << pluginId.toLocal8Bit().data() << std::endl; + continue; + } + + const RealTimePluginDescriptor *descriptor = + factory->getPluginDescriptor(pluginId); + + if (!descriptor) { + std::cerr << "WARNING: TransformFactory::populateTransforms: Failed to query plugin " << pluginId.toLocal8Bit().data() << std::endl; + continue; + } + + if (descriptor->controlOutputPortCount == 0 || + descriptor->audioInputPortCount == 0) continue; + +// std::cout << "TransformFactory::populateRealTimePlugins: plugin " << pluginId.toStdString() << " has " << descriptor->controlOutputPortCount << " output ports" << std::endl; + + QString pluginDescription = descriptor->name.c_str(); + + for (size_t j = 0; j < descriptor->controlOutputPortCount; ++j) { + + QString transformName = QString("%1:%2").arg(pluginId).arg(j); + QString userDescription; + QString units; + + if (j < descriptor->controlOutputPortNames.size() && + descriptor->controlOutputPortNames[j] != "") { + + QString portName = descriptor->controlOutputPortNames[j].c_str(); + + userDescription = tr("%1: %2") + .arg(pluginDescription) + .arg(portName); + + if (unitRE.indexIn(portName) >= 0) { + units = unitRE.cap(1); + } + + } else if (descriptor->controlOutputPortCount > 1) { + + userDescription = tr("%1: Output %2") + .arg(pluginDescription) + .arg(j + 1); + + } else { + + userDescription = pluginDescription; + } + + + bool configurable = (descriptor->parameterCount > 0); + + transforms[transformName] = + TransformDesc(tr("Other Plugins"), + transformName, + userDescription, + userDescription, + descriptor->maker.c_str(), + units, + configurable); + } + } +} + +QString +TransformFactory::getTransformDescription(TransformName name) +{ + if (m_transforms.find(name) != m_transforms.end()) { + return m_transforms[name].description; + } else return ""; +} + +QString +TransformFactory::getTransformFriendlyName(TransformName name) +{ + if (m_transforms.find(name) != m_transforms.end()) { + return m_transforms[name].friendlyName; + } else return ""; +} + +QString +TransformFactory::getTransformUnits(TransformName name) +{ + if (m_transforms.find(name) != m_transforms.end()) { + return m_transforms[name].units; + } else return ""; +} + +bool +TransformFactory::isTransformConfigurable(TransformName name) +{ + if (m_transforms.find(name) != m_transforms.end()) { + return m_transforms[name].configurable; + } else return false; +} + +bool +TransformFactory::getTransformChannelRange(TransformName name, + int &min, int &max) +{ + QString id = name.section(':', 0, 2); + + if (FeatureExtractionPluginFactory::instanceFor(id)) { + + Vamp::Plugin *plugin = + FeatureExtractionPluginFactory::instanceFor(id)-> + instantiatePlugin(id, 48000); + if (!plugin) return false; + + min = plugin->getMinChannelCount(); + max = plugin->getMaxChannelCount(); + delete plugin; + + return true; + + } else if (RealTimePluginFactory::instanceFor(id)) { + + const RealTimePluginDescriptor *descriptor = + RealTimePluginFactory::instanceFor(id)-> + getPluginDescriptor(id); + if (!descriptor) return false; + + min = descriptor->audioInputPortCount; + max = descriptor->audioInputPortCount; + + return true; + } + + return false; +} + +bool +TransformFactory::getChannelRange(TransformName name, Vamp::PluginBase *plugin, + int &minChannels, int &maxChannels) +{ + Vamp::Plugin *vp = 0; + if ((vp = dynamic_cast(plugin))) { + minChannels = vp->getMinChannelCount(); + maxChannels = vp->getMaxChannelCount(); + return true; + } else { + return getTransformChannelRange(name, minChannels, maxChannels); + } +} + +bool +TransformFactory::getConfigurationForTransform(TransformName name, + Model *inputModel, + int &channel, + QString &configurationXml) +{ + QString id = name.section(':', 0, 2); + QString output = name.section(':', 3); + + bool ok = false; + configurationXml = m_lastConfigurations[name]; + +// std::cerr << "last configuration: " << configurationXml.toStdString() << std::endl; + + Vamp::PluginBase *plugin = 0; + + if (FeatureExtractionPluginFactory::instanceFor(id)) { + + plugin = FeatureExtractionPluginFactory::instanceFor(id)->instantiatePlugin + (id, inputModel->getSampleRate()); + + } else if (RealTimePluginFactory::instanceFor(id)) { + + plugin = RealTimePluginFactory::instanceFor(id)->instantiatePlugin + (id, 0, 0, inputModel->getSampleRate(), 1024, 1); + } + + if (plugin) { + if (configurationXml != "") { + PluginXml(plugin).setParametersFromXml(configurationXml); + } + + int sourceChannels = 1; + if (dynamic_cast(inputModel)) { + sourceChannels = dynamic_cast(inputModel) + ->getChannelCount(); + } + + int minChannels = 1, maxChannels = sourceChannels; + getChannelRange(name, plugin, minChannels, maxChannels); + + int targetChannels = sourceChannels; + if (sourceChannels < minChannels) targetChannels = minChannels; + if (sourceChannels > maxChannels) targetChannels = maxChannels; + + int defaultChannel = channel; + + PluginParameterDialog *dialog = new PluginParameterDialog(plugin, + sourceChannels, + targetChannels, + defaultChannel, + output); + if (dialog->exec() == QDialog::Accepted) { + ok = true; + } + configurationXml = PluginXml(plugin).toXmlString(); + channel = dialog->getChannel(); + delete dialog; + delete plugin; + } + + if (ok) m_lastConfigurations[name] = configurationXml; + + return ok; +} + +Transform * +TransformFactory::createTransform(TransformName name, Model *inputModel, + int channel, QString configurationXml, bool start) +{ + Transform *transform = 0; + + //!!! use channel + + QString id = name.section(':', 0, 2); + QString output = name.section(':', 3); + + if (FeatureExtractionPluginFactory::instanceFor(id)) { + transform = new FeatureExtractionPluginTransform(inputModel, + id, + channel, + configurationXml, + output); + } else if (RealTimePluginFactory::instanceFor(id)) { + transform = new RealTimePluginTransform(inputModel, + id, + channel, + configurationXml, + getTransformUnits(name), + output.toInt()); + } else { + std::cerr << "TransformFactory::createTransform: Unknown transform \"" + << name.toStdString() << "\"" << std::endl; + return transform; + } + + if (start && transform) transform->start(); + transform->setObjectName(name); + return transform; +} + +Model * +TransformFactory::transform(TransformName name, Model *inputModel, + int channel, QString configurationXml) +{ + Transform *t = createTransform(name, inputModel, channel, + configurationXml, false); + + if (!t) return 0; + + connect(t, SIGNAL(finished()), this, SLOT(transformFinished())); + + t->start(); + return t->detachOutputModel(); +} + +void +TransformFactory::transformFinished() +{ + QObject *s = sender(); + Transform *transform = dynamic_cast(s); + + if (!transform) { + std::cerr << "WARNING: TransformFactory::transformFinished: sender is not a transform" << std::endl; + return; + } + + transform->wait(); // unnecessary but reassuring + delete transform; +} + diff -r 000000000000 -r cd5d7ff8ef38 transform/TransformFactory.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/transform/TransformFactory.h Mon Jul 31 12:03:45 2006 +0000 @@ -0,0 +1,155 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +/* + Sonic Visualiser + An audio file viewer and annotation editor. + Centre for Digital Music, Queen Mary, University of London. + This file copyright 2006 Chris Cannam. + + This program 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. See the file + COPYING included with this distribution for more information. +*/ + +#ifndef _TRANSFORM_FACTORY_H_ +#define _TRANSFORM_FACTORY_H_ + +#include "Transform.h" + +#include + +namespace Vamp { class PluginBase; } + +class TransformFactory : public QObject +{ + Q_OBJECT + +public: + virtual ~TransformFactory(); + + static TransformFactory *getInstance(); + + // The name is intended to be computer-referenceable, and unique + // within the application. The description is intended to be + // human readable. In principle it doesn't have to be unique, but + // the factory will add suffixes to ensure that it is, all the + // same (just to avoid user confusion). The friendly name is a + // shorter version of the description. The type is also intended + // to be user-readable, for use in menus. + + struct TransformDesc { + TransformDesc() { } + TransformDesc(QString _type, TransformName _name, QString _description, + QString _friendlyName, QString _maker, + QString _units, bool _configurable) : + type(_type), name(_name), description(_description), + friendlyName(_friendlyName), + maker(_maker), units(_units), configurable(_configurable) { } + QString type; + TransformName name; + QString description; + QString friendlyName; + QString maker; + QString units; + bool configurable; + }; + typedef std::vector TransformList; + + TransformList getAllTransforms(); + + std::vector getAllTransformTypes(); + + /** + * Get a configuration XML string for the given transform (by + * asking the user, most likely). Returns true if the transform + * is acceptable, false if the operation should be cancelled. + */ + bool getConfigurationForTransform(TransformName name, Model *inputModel, + int &channel, + QString &configurationXml); + + /** + * Return the output model resulting from applying the named + * transform to the given input model. The transform may still be + * working in the background when the model is returned; check the + * output model's isReady completion status for more details. + * + * If the transform is unknown or the input model is not an + * appropriate type for the given transform, or if some other + * problem occurs, return 0. + * + * The returned model is owned by the caller and must be deleted + * when no longer needed. + */ + Model *transform(TransformName name, Model *inputModel, + int channel, QString configurationXml = ""); + + /** + * Full description of a transform, suitable for putting on a menu. + */ + QString getTransformDescription(TransformName name); + + /** + * Brief but friendly description of a transform, suitable for use + * as the name of the output layer. + */ + QString getTransformFriendlyName(TransformName name); + + QString getTransformUnits(TransformName name); + + /** + * Return true if the transform has any configurable parameters, + * i.e. if getConfigurationForTransform can ever return a non-trivial + * (not equivalent to empty) configuration string. + */ + bool isTransformConfigurable(TransformName name); + + /** + * If the transform has a prescribed number or range of channel + * inputs, return true and set minChannels and maxChannels to the + * minimum and maximum number of channel inputs the transform can + * accept. + */ + bool getTransformChannelRange(TransformName name, + int &minChannels, int &maxChannels); + + //!!! Need some way to indicate that the input model has changed / + //been deleted so as not to blow up backgrounded transform! -- Or + //indeed, if the output model has been deleted -- could equally + //well happen! + + //!!! Need transform category! + +protected slots: + void transformFinished(); + +protected: + Transform *createTransform(TransformName name, Model *inputModel, + int channel, QString configurationXml, bool start); + + struct TransformIdent + { + TransformName name; + QString configurationXml; + }; + + typedef std::map TransformConfigurationMap; + TransformConfigurationMap m_lastConfigurations; + + typedef std::map TransformDescriptionMap; + TransformDescriptionMap m_transforms; + + void populateTransforms(); + void populateFeatureExtractionPlugins(TransformDescriptionMap &); + void populateRealTimePlugins(TransformDescriptionMap &); + + bool getChannelRange(TransformName name, + Vamp::PluginBase *plugin, int &min, int &max); + + static TransformFactory *m_instance; +}; + + +#endif