diff --git a/audioio/AudioCallbackPlaySource.cpp b/audioio/AudioCallbackPlaySource.cpp new file mode 100644 index 0000000..b367772 --- /dev/null +++ b/audioio/AudioCallbackPlaySource.cpp @@ -0,0 +1,1493 @@ +/* -*- 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 and QMUL. + + 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 "data/model/Model.h" +#include "view/ViewManager.h" +#include "base/PlayParameterRepository.h" +#include "base/Preferences.h" +#include "data/model/DenseTimeValueModel.h" +#include "data/model/WaveFileModel.h" +#include "data/model/SparseOneDimensionalModel.h" +#include "plugin/RealTimePluginInstance.h" +#include "PhaseVocoderTimeStretcher.h" + +#include +#include + +//#define DEBUG_AUDIO_PLAY_SOURCE 1 +//#define DEBUG_AUDIO_PLAY_SOURCE_PLAYING 1 + +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_auditioningPlugin(0), + m_auditioningPluginBypassed(false), + m_timeStretcher(0), + m_fillThread(0), + m_converter(0), + m_crapConverter(0), + m_resampleQuality(Preferences::getInstance()->getResampleQuality()) +{ + 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 *))); + + connect(Preferences::getInstance(), + SIGNAL(propertyChanged(PropertyContainer::PropertyName)), + this, SLOT(preferenceChanged(PropertyContainer::PropertyName))); +} + +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); + m_pluginScavenger.scavenge(true); + m_timeStretcherScavenger.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; + } + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "Adding model with " << modelChannels << " channels " << std::endl; +#endif + + 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) { + // Only wave file models can be considered conflicting -- + // writable wave file models are derived and we shouldn't + // take their rates into account. Also, don't give any + // particular weight to a file that's already playing at + // the wrong rate anyway + WaveFileModel *wfm = dynamic_cast(*i); + if (wfm && wfm != dtvm && + wfm->getSampleRate() != model->getSampleRate() && + wfm->getSampleRate() == m_sourceSampleRate) { + std::cerr << "AudioCallbackPlaySource::addModel: Conflicting wave file 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); + src_delete(m_crapConverter); + m_converter = 0; + m_crapConverter = 0; + } + } + + m_mutex.unlock(); + + m_audioGenerator->setTargetChannelCount(getTargetChannelCount()); + + if (!m_fillThread) { + m_fillThread = new FillThread(*this); + m_fillThread->start(); + } + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "AudioCallbackPlaySource::addModel: now have " << m_models.size() << " model(s) -- emitting modelReplaced" << std::endl; +#endif + + if (buffersChanged || srChanged) { + emit modelReplaced(); + } + + connect(model, SIGNAL(modelChanged(size_t, size_t)), + this, SLOT(modelChanged(size_t, size_t))); + + m_condition.wakeAll(); +} + +void +AudioCallbackPlaySource::modelChanged(size_t startFrame, size_t endFrame) +{ +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "AudioCallbackPlaySource::modelChanged(" << startFrame << "," << endFrame << ")" << std::endl; +#endif + if (endFrame > m_lastModelEndFrame) m_lastModelEndFrame = endFrame; +} + +void +AudioCallbackPlaySource::removeModel(Model *model) +{ + m_mutex.lock(); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "AudioCallbackPlaySource::removeModel(" << model << ")" << std::endl; +#endif + + disconnect(model, SIGNAL(modelChanged(size_t, size_t)), + this, SLOT(modelChanged(size_t, size_t))); + + m_models.erase(model); + + if (m_models.empty()) { + if (m_converter) { + src_delete(m_converter); + src_delete(m_crapConverter); + m_converter = 0; + m_crapConverter = 0; + } + m_sourceSampleRate = 0; + } + + size_t lastEnd = 0; + for (std::set::const_iterator i = m_models.begin(); + i != m_models.end(); ++i) { +// std::cout << "AudioCallbackPlaySource::removeModel(" << model << "): checking end frame on model " << *i << std::endl; + if ((*i)->getEndFrame() > lastEnd) lastEnd = (*i)->getEndFrame(); +// std::cout << "(done, lastEnd now " << lastEnd << ")" << std::endl; + } + m_lastModelEndFrame = lastEnd; + + m_mutex.unlock(); + + m_audioGenerator->removeModel(model); + + clearRingBuffers(); +} + +void +AudioCallbackPlaySource::clearModels() +{ + m_mutex.lock(); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "AudioCallbackPlaySource::clearModels()" << std::endl; +#endif + + m_models.clear(); + + if (m_converter) { + src_delete(m_converter); + src_delete(m_crapConverter); + m_converter = 0; + m_crapConverter = 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::cout << "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); + if (m_crapConverter) src_reset(m_crapConverter); + } else { + if (m_converter) src_reset(m_converter); + if (m_crapConverter) src_reset(m_crapConverter); + 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 *) +{ + clearRingBuffers(); +} + +void +AudioCallbackPlaySource::preferenceChanged(PropertyContainer::PropertyName n) +{ + if (n == "Resample Quality") { + setResampleQuality(Preferences::getInstance()->getResampleQuality()); + } +} + +void +AudioCallbackPlaySource::audioProcessingOverload() +{ + RealTimePluginInstance *ap = m_auditioningPlugin; + if (ap && m_playing && !m_auditioningPluginBypassed) { + m_auditioningPluginBypassed = true; + emit audioOverloadPluginDisabled(); + } +} + +void +AudioCallbackPlaySource::setTargetBlockSize(size_t size) +{ +// std::cout << "AudioCallbackPlaySource::setTargetBlockSize() -> " << size << std::endl; + assert(size < m_ringBufferSize); + m_blockSize = size; +} + +size_t +AudioCallbackPlaySource::getTargetBlockSize() const +{ +// std::cout << "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); + + PhaseVocoderTimeStretcher *timeStretcher = m_timeStretcher; + if (timeStretcher) { + latency += timeStretcher->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::cout << "getCurrentPlayingFrame: f=" << f << ", latency=" << latency << ", rangeEnd=" << rangeEnd << std::endl; + + if (i == selections.end()) { + --i; + if (i->getEndFrame() + latency < f) { +// std::cout << "framePlaying = " << framePlaying << ", rangeEnd = " << rangeEnd << std::endl; + + if (!looping && (framePlaying > rangeEnd)) { +// std::cout << "STOPPING" << std::endl; + stop(); + return rangeEnd; + } else { + return framePlaying; + } + } else { +// std::cout << "latency <- " << latency << "-(" << f << "-" << i->getEndFrame() << ")" << std::endl; + latency -= (f - i->getEndFrame()); + f = i->getEndFrame(); + } + } + +// std::cout << "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; + initialiseConverter(); +} + +void +AudioCallbackPlaySource::initialiseConverter() +{ + m_mutex.lock(); + + if (m_converter) { + src_delete(m_converter); + src_delete(m_crapConverter); + m_converter = 0; + m_crapConverter = 0; + } + + if (getSourceSampleRate() != getTargetSampleRate()) { + + int err = 0; + + m_converter = src_new(m_resampleQuality == 2 ? SRC_SINC_BEST_QUALITY : + m_resampleQuality == 1 ? SRC_SINC_MEDIUM_QUALITY : + m_resampleQuality == 0 ? SRC_SINC_FASTEST : + SRC_SINC_MEDIUM_QUALITY, + getTargetChannelCount(), &err); + + if (m_converter) { + m_crapConverter = src_new(SRC_LINEAR, + getTargetChannelCount(), + &err); + } + + if (!m_converter || !m_crapConverter) { + std::cerr + << "AudioCallbackPlaySource::setModel: ERROR in creating samplerate converter: " + << src_strerror(err) << std::endl; + + if (m_converter) { + src_delete(m_converter); + m_converter = 0; + } + + if (m_crapConverter) { + src_delete(m_crapConverter); + m_crapConverter = 0; + } + + m_mutex.unlock(); + + emit sampleRateMismatch(getSourceSampleRate(), + getTargetSampleRate(), + false); + } else { + + m_mutex.unlock(); + + emit sampleRateMismatch(getSourceSampleRate(), + getTargetSampleRate(), + true); + } + } else { + m_mutex.unlock(); + } +} + +void +AudioCallbackPlaySource::setResampleQuality(int q) +{ + if (q == m_resampleQuality) return; + m_resampleQuality = q; + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "AudioCallbackPlaySource::setResampleQuality: setting to " + << m_resampleQuality << std::endl; +#endif + + initialiseConverter(); +} + +void +AudioCallbackPlaySource::setAuditioningPlugin(RealTimePluginInstance *plugin) +{ + RealTimePluginInstance *formerPlugin = m_auditioningPlugin; + m_auditioningPlugin = plugin; + m_auditioningPluginBypassed = false; + if (formerPlugin) m_pluginScavenger.claim(formerPlugin); +} + +void +AudioCallbackPlaySource::setSoloModelSet(std::set s) +{ + m_audioGenerator->setSoloModelSet(s); + clearRingBuffers(); +} + +void +AudioCallbackPlaySource::clearSoloModelSet() +{ + m_audioGenerator->clearSoloModelSet(); + clearRingBuffers(); +} + +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; +} + +void +AudioCallbackPlaySource::setTimeStretch(float factor, bool sharpen, bool mono) +{ + // Avoid locks -- create, assign, mark old one for scavenging + // later (as a call to getSourceSamples may still be using it) + + PhaseVocoderTimeStretcher *existingStretcher = m_timeStretcher; + + size_t channels = getTargetChannelCount(); + if (mono) channels = 1; + + if (existingStretcher && + existingStretcher->getRatio() == factor && + existingStretcher->getSharpening() == sharpen && + existingStretcher->getChannelCount() == channels) { + return; + } + + if (factor != 1) { + + if (existingStretcher && + existingStretcher->getSharpening() == sharpen && + existingStretcher->getChannelCount() == channels) { + existingStretcher->setRatio(factor); + return; + } + + PhaseVocoderTimeStretcher *newStretcher = new PhaseVocoderTimeStretcher + (getTargetSampleRate(), + channels, + factor, + sharpen, + getTargetBlockSize()); + + 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; + } + + // Ensure that all buffers have at least the amount of data we + // need -- else reduce the size of our requests correspondingly + + for (size_t ch = 0; ch < getTargetChannelCount(); ++ch) { + + RingBuffer *rb = getReadRingBuffer(ch); + + if (!rb) { + std::cerr << "WARNING: AudioCallbackPlaySource::getSourceSamples: " + << "No ring buffer available for channel " << ch + << ", returning no data here" << std::endl; + count = 0; + break; + } + + size_t rs = rb->getReadSpace(); + if (rs < count) { +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cerr << "WARNING: AudioCallbackPlaySource::getSourceSamples: " + << "Ring buffer for channel " << ch << " has only " + << rs << " (of " << count << ") samples available, " + << "reducing request size" << std::endl; +#endif + count = rs; + } + } + + if (count == 0) return 0; + + PhaseVocoderTimeStretcher *ts = m_timeStretcher; + + if (!ts || ts->getRatio() == 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 << " (of " << count << ") 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; + } + } + } + + applyAuditioningEffect(count, buffer); + + m_condition.wakeAll(); + return got; + } + + float ratio = ts->getRatio(); + +// std::cout << "ratio = " << ratio << std::endl; + + size_t channels = getTargetChannelCount(); + bool mix = (channels > 1 && ts->getChannelCount() == 1); + + size_t available; + + int warned = 0; + + // We want output blocks of e.g. 1024 (probably fixed, certainly + // bounded). We can provide input blocks of any size (unbounded) + // at the timestretcher's request. The input block for a given + // output is approx output / ratio, but we can't predict it + // exactly, for an adaptive timestretcher. The stretcher will + // need some additional buffer space. See the time stretcher code + // and comments. + + while ((available = ts->getAvailableOutputSamples()) < count) { + + size_t reqd = lrintf((count - available) / ratio); + reqd = std::max(reqd, ts->getRequiredInputSamples()); + if (reqd == 0) reqd = 1; + + float *ib[channels]; + + size_t got = reqd; + + if (mix) { + for (size_t c = 0; c < channels; ++c) { + if (c == 0) ib[c] = new float[reqd]; //!!! fix -- this is a rt function + else ib[c] = 0; + RingBuffer *rb = getReadRingBuffer(c); + if (rb) { + size_t gotHere; + if (c > 0) gotHere = rb->readAdding(ib[0], got); + else gotHere = rb->read(ib[0], got); + if (gotHere < got) got = gotHere; + } + } + } else { + for (size_t c = 0; c < channels; ++c) { + ib[c] = new float[reqd]; //!!! fix -- this is a rt function + RingBuffer *rb = getReadRingBuffer(c); + if (rb) { + size_t gotHere = rb->read(ib[c], got); + if (gotHere < got) got = gotHere; + } + } + } + + if (got < reqd) { + std::cerr << "WARNING: Read underrun in playback (" + << got << " < " << reqd << ")" << std::endl; + } + + ts->putInput(ib, got); + + for (size_t c = 0; c < channels; ++c) { + delete[] ib[c]; + } + + if (got == 0) break; + + if (ts->getAvailableOutputSamples() == available) { + std::cerr << "WARNING: AudioCallbackPlaySource::getSamples: Added " << got << " samples to time stretcher, created no new available output samples (warned = " << warned << ")" << std::endl; + if (++warned == 5) break; + } + } + + ts->getOutput(buffer, count); + + if (mix) { + for (size_t c = 1; c < channels; ++c) { + for (size_t i = 0; i < count; ++i) { + buffer[c][i] = buffer[0][i] / channels; + } + } + for (size_t i = 0; i < count; ++i) { + buffer[0][i] /= channels; + } + } + + applyAuditioningEffect(count, buffer); + + m_condition.wakeAll(); + + return count; +} + +void +AudioCallbackPlaySource::applyAuditioningEffect(size_t count, float **buffers) +{ + if (m_auditioningPluginBypassed) return; + RealTimePluginInstance *plugin = m_auditioningPlugin; + if (!plugin) return; + + if (plugin->getAudioInputCount() != getTargetChannelCount()) { +// std::cerr << "plugin input count " << plugin->getAudioInputCount() +// << " != our channel count " << getTargetChannelCount() +// << std::endl; + return; + } + if (plugin->getAudioOutputCount() != getTargetChannelCount()) { +// std::cerr << "plugin output count " << plugin->getAudioOutputCount() +// << " != our channel count " << getTargetChannelCount() +// << std::endl; + return; + } + if (plugin->getBufferSize() != count) { +// std::cerr << "plugin buffer size " << plugin->getBufferSize() +// << " != our block size " << count +// << std::endl; + return; + } + + float **ib = plugin->getAudioInputBuffers(); + float **ob = plugin->getAudioOutputBuffers(); + + for (size_t c = 0; c < getTargetChannelCount(); ++c) { + for (size_t i = 0; i < count; ++i) { + ib[c][i] = buffers[c][i]; + } + } + + plugin->run(Vamp::RealTime::zeroTime); + + for (size_t c = 0; c < getTargetChannelCount(); ++c) { + for (size_t i = 0; i < count; ++i) { + buffers[c][i] = ob[c][i]; + } + } +} + +// 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 = 0; + + if (m_timeStretcher && m_timeStretcher->getRatio() < 0.4) { +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "Using crappy converter" << std::endl; +#endif + err = src_process(m_crapConverter, &data); + } else { + err = src_process(m_converter, &data); + } + + 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::cout << "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) { + size_t actual = wb->write(bufferPtrs[c], got); +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "Wrote " << actual << " samples for ch " << c << ", now " + << wb->getReadSpace() << " to read" + << std::endl; +#endif + if (actual < got) { + std::cerr << "WARNING: Buffer overrun in channel " << c + << ": wrote " << actual << " of " << got + << " samples" << std::endl; + } + } + } + + 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::cout << "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::cout << "chunkStart " << chunkStart << ", chunkSize " << chunkSize << ", nextChunkStart " << nextChunkStart << ", frame " << frame << ", count " << count << ", processed " << processed << std::endl; + + if (!chunkSize) { +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "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::cout << "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::cout << "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::cout << "rs = " << rs << std::endl; + if (rs < rf) rf -= rs; + else rf = 0; + } + + //std::cout << "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::cout << "wrs = " << wrs << std::endl; + + if (wrs < wf) wf -= wrs; + else wf = 0; +// std::cout << "wf = " << wf << std::endl; + + if (wf < rf) skip = rf - wf; + if (skip == 0) break; + } + +// std::cout << "skipping " << skip << std::endl; + wb->skip(skip); + } + } + + m_bufferScavenger.claim(m_readBuffers); + m_readBuffers = m_writeBuffers; + m_readBufferFill = m_writeBufferFill; +// std::cout << "unified" << std::endl; +} + +void +AudioCallbackPlaySource::FillThread::run() +{ + AudioCallbackPlaySource &s(m_source); + +#ifdef DEBUG_AUDIO_PLAY_SOURCE + std::cout << "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_pluginScavenger.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 + if (!s.m_playing) std::cout << std::endl; + 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(); +} + diff --git a/audioio/AudioCallbackPlaySource.h b/audioio/AudioCallbackPlaySource.h new file mode 100644 index 0000000..bf1c742 --- /dev/null +++ b/audioio/AudioCallbackPlaySource.h @@ -0,0 +1,344 @@ +/* -*- 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 and QMUL. + + 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/PropertyContainer.h" +#include "base/Scavenger.h" + +#include +#include +#include + +#include "base/Thread.h" + +#include + +#include +#include + +class Model; +class ViewManager; +class AudioGenerator; +class PlayParameters; +class PhaseVocoderTimeStretcher; +class RealTimePluginInstance; + +/** + * 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(); + + /** + * Return the frame at which playback is expected to end (if not looping). + */ + virtual size_t getPlayEndFrame() { return m_lastModelEndFrame; } + + /** + * 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. + */ + virtual 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); + + /** + * Set the time stretcher factor (i.e. playback speed). Also + * specify whether the time stretcher will be variable rate + * (sharpening transients), and whether time stretching will be + * carried out on data mixed down to mono for speed. + */ + void setTimeStretch(float factor, bool sharpen, bool mono); + + /** + * Set the resampler quality, 0 - 2 where 0 is fastest and 2 is + * highest quality. + */ + void setResampleQuality(int q); + + /** + * Set a single real-time plugin as a processing effect for + * auditioning during playback. + * + * The plugin must have been initialised with + * getTargetChannelCount() channels and a getTargetBlockSize() + * sample frame processing block size. + * + * This playback source takes ownership of the plugin, which will + * be deleted at some point after the following call to + * setAuditioningPlugin (depending on real-time constraints). + * + * Pass a null pointer to remove the current auditioning plugin, + * if any. + */ + void setAuditioningPlugin(RealTimePluginInstance *plugin); + + /** + * Specify that only the given set of models should be played. + */ + void setSoloModelSet(std::sets); + + /** + * Specify that all models should be played as normal (if not + * muted). + */ + void clearSoloModelSet(); + +signals: + void modelReplaced(); + + void playStatusChanged(bool isPlaying); + + void sampleRateMismatch(size_t requested, size_t available, bool willResample); + + void audioOverloadPluginDisabled(); + +public slots: + void audioProcessingOverload(); + +protected slots: + void selectionChanged(); + void playLoopModeChanged(); + void playSelectionModeChanged(); + void playParametersChanged(PlayParameters *); + void preferenceChanged(PropertyContainer::PropertyName); + void modelChanged(size_t startFrame, size_t endFrame); + +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; + RealTimePluginInstance *m_auditioningPlugin; + bool m_auditioningPluginBypassed; + Scavenger m_pluginScavenger; + + 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(); + + PhaseVocoderTimeStretcher *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); + + // Called from getSourceSamples. + void applyAuditioningEffect(size_t count, float **buffers); + + class FillThread : public Thread + { + public: + FillThread(AudioCallbackPlaySource &source) : + Thread(Thread::NonRTThread), + m_source(source) { } + + virtual void run(); + + protected: + AudioCallbackPlaySource &m_source; + }; + + QMutex m_mutex; + QWaitCondition m_condition; + FillThread *m_fillThread; + SRC_STATE *m_converter; + SRC_STATE *m_crapConverter; // for use when playing very fast + int m_resampleQuality; + void initialiseConverter(); +}; + +#endif + + diff --git a/audioio/AudioCallbackPlayTarget.cpp b/audioio/AudioCallbackPlayTarget.cpp new file mode 100644 index 0000000..e9e30d2 --- /dev/null +++ b/audioio/AudioCallbackPlayTarget.cpp @@ -0,0 +1,40 @@ +/* -*- 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; +} + diff --git a/audioio/AudioCallbackPlayTarget.h b/audioio/AudioCallbackPlayTarget.h new file mode 100644 index 0000000..13dacae --- /dev/null +++ b/audioio/AudioCallbackPlayTarget.h @@ -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 --git a/audioio/AudioCoreAudioTarget.cpp b/audioio/AudioCoreAudioTarget.cpp new file mode 100644 index 0000000..0968026 --- /dev/null +++ b/audioio/AudioCoreAudioTarget.cpp @@ -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 --git a/audioio/AudioCoreAudioTarget.h b/audioio/AudioCoreAudioTarget.h new file mode 100644 index 0000000..a616466 --- /dev/null +++ b/audioio/AudioCoreAudioTarget.h @@ -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 --git a/audioio/AudioGenerator.cpp b/audioio/AudioGenerator.cpp new file mode 100644 index 0000000..114189b --- /dev/null +++ b/audioio/AudioGenerator.cpp @@ -0,0 +1,799 @@ +/* -*- 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 "data/model/NoteModel.h" +#include "data/model/DenseTimeValueModel.h" +#include "data/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), + m_soloing(false) +{ + 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; +} + +void +AudioGenerator::setSoloModelSet(std::set s) +{ + QMutexLocker locker(&m_mutex); + + std::cerr << "setting solo set" << std::endl; + + m_soloModelSet = s; + m_soloing = true; +} + +void +AudioGenerator::clearSoloModelSet() +{ + QMutexLocker locker(&m_mutex); + + m_soloModelSet.clear(); + m_soloing = false; +} + +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) { +#ifdef DEBUG_AUDIO_GENERATOR + std::cout << "AudioGenerator::mixModel(" << model << "): muted" << std::endl; +#endif + return frameCount; + } + + if (m_soloing) { + if (m_soloModelSet.find(model) == m_soloModelSet.end()) { +#ifdef DEBUG_AUDIO_GENERATOR + std::cout << "AudioGenerator::mixModel(" << model << "): not one of the solo'd models" << std::endl; +#endif + 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 --git a/audioio/AudioGenerator.h b/audioio/AudioGenerator.h new file mode 100644 index 0000000..837a1ec --- /dev/null +++ b/audioio/AudioGenerator.h @@ -0,0 +1,158 @@ +/* -*- 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); + + /** + * Specify that only the given set of models should be played. + */ + virtual void setSoloModelSet(std::sets); + + /** + * Specify that all models should be played as normal (if not + * muted). + */ + virtual void clearSoloModelSet(); + +protected slots: + void playPluginIdChanged(const Model *, QString); + void playPluginConfigurationChanged(const Model *, QString); + +protected: + size_t m_sourceSampleRate; + size_t m_targetChannelCount; + + bool m_soloing; + std::set m_soloModelSet; + + 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 --git a/audioio/AudioJACKTarget.cpp b/audioio/AudioJACKTarget.cpp new file mode 100644 index 0000000..8f4b96e --- /dev/null +++ b/audioio/AudioJACKTarget.cpp @@ -0,0 +1,400 @@ +/* -*- 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 + +#ifdef BUILD_STATIC +#ifdef Q_OS_LINUX + +// Some lunacy to enable JACK support in static builds. JACK isn't +// supposed to be linked statically, because it depends on a +// consistent shared memory layout between client library and daemon, +// so it's very fragile in the face of version mismatches. +// +// Therefore for static builds on Linux we avoid linking against JACK +// at all during the build, instead using dlopen and runtime symbol +// lookup to switch on JACK support at runtime. The following big +// mess (down to the #endifs) is the code that implements this. + +static void *symbol(const char *name) +{ + static bool attempted = false; + static void *library = 0; + static std::map symbols; + if (symbols.find(name) != symbols.end()) return symbols[name]; + if (!library) { + if (!attempted) { + library = ::dlopen("libjack.so.1", RTLD_NOW); + if (!library) library = ::dlopen("libjack.so.0", RTLD_NOW); + if (!library) library = ::dlopen("libjack.so", RTLD_NOW); + if (!library) { + std::cerr << "WARNING: AudioJACKTarget: Failed to load JACK library: " + << ::dlerror() << " (tried .so, .so.0, .so.1)" + << std::endl; + } + attempted = true; + } + if (!library) return 0; + } + void *symbol = ::dlsym(library, name); + if (!symbol) { + std::cerr << "WARNING: AudioJACKTarget: Failed to locate symbol " + << name << ": " << ::dlerror() << std::endl; + } + symbols[name] = symbol; + return symbol; +} + +static int dynamic_jack_set_process_callback(jack_client_t *client, + JackProcessCallback process_callback, + void *arg) +{ + typedef int (*func)(jack_client_t *client, + JackProcessCallback process_callback, + void *arg); + void *s = symbol("jack_set_process_callback"); + if (!s) return 1; + func f = (func)s; + return f(client, process_callback, arg); +} + +static int dynamic_jack_set_xrun_callback(jack_client_t *client, + JackXRunCallback xrun_callback, + void *arg) +{ + typedef int (*func)(jack_client_t *client, + JackXRunCallback xrun_callback, + void *arg); + void *s = symbol("jack_set_xrun_callback"); + if (!s) return 1; + func f = (func)s; + return f(client, xrun_callback, arg); +} + +static const char **dynamic_jack_get_ports(jack_client_t *client, + const char *port_name_pattern, + const char *type_name_pattern, + unsigned long flags) +{ + typedef const char **(*func)(jack_client_t *client, + const char *port_name_pattern, + const char *type_name_pattern, + unsigned long flags); + void *s = symbol("jack_get_ports"); + if (!s) return 0; + func f = (func)s; + return f(client, port_name_pattern, type_name_pattern, flags); +} + +static jack_port_t *dynamic_jack_port_register(jack_client_t *client, + const char *port_name, + const char *port_type, + unsigned long flags, + unsigned long buffer_size) +{ + typedef jack_port_t *(*func)(jack_client_t *client, + const char *port_name, + const char *port_type, + unsigned long flags, + unsigned long buffer_size); + void *s = symbol("jack_port_register"); + if (!s) return 0; + func f = (func)s; + return f(client, port_name, port_type, flags, buffer_size); +} + +static int dynamic_jack_connect(jack_client_t *client, + const char *source, + const char *dest) +{ + typedef int (*func)(jack_client_t *client, + const char *source, + const char *dest); + void *s = symbol("jack_connect"); + if (!s) return 1; + func f = (func)s; + return f(client, source, dest); +} + +static void *dynamic_jack_port_get_buffer(jack_port_t *port, + jack_nframes_t sz) +{ + typedef void *(*func)(jack_port_t *, jack_nframes_t); + void *s = symbol("jack_port_get_buffer"); + if (!s) return 0; + func f = (func)s; + return f(port, sz); +} + +static int dynamic_jack_port_unregister(jack_client_t *client, + jack_port_t *port) +{ + typedef int(*func)(jack_client_t *, jack_port_t *); + void *s = symbol("jack_port_unregister"); + if (!s) return 0; + func f = (func)s; + return f(client, port); +} + +#define dynamic1(rv, name, argtype, failval) \ + static rv dynamic_##name(argtype arg) { \ + typedef rv (*func) (argtype); \ + void *s = symbol(#name); \ + if (!s) return failval; \ + func f = (func) s; \ + return f(arg); \ + } + +dynamic1(jack_client_t *, jack_client_new, const char *, 0); +dynamic1(jack_nframes_t, jack_get_buffer_size, jack_client_t *, 0); +dynamic1(jack_nframes_t, jack_get_sample_rate, jack_client_t *, 0); +dynamic1(int, jack_activate, jack_client_t *, 1); +dynamic1(int, jack_deactivate, jack_client_t *, 1); +dynamic1(int, jack_client_close, jack_client_t *, 1); +dynamic1(jack_nframes_t, jack_port_get_latency, jack_port_t *, 0); +dynamic1(const char *, jack_port_name, const jack_port_t *, 0); + +#define jack_client_new dynamic_jack_client_new +#define jack_get_buffer_size dynamic_jack_get_buffer_size +#define jack_get_sample_rate dynamic_jack_get_sample_rate +#define jack_set_process_callback dynamic_jack_set_process_callback +#define jack_set_xrun_callback dynamic_jack_set_xrun_callback +#define jack_activate dynamic_jack_activate +#define jack_deactivate dynamic_jack_deactivate +#define jack_client_close dynamic_jack_client_close +#define jack_get_ports dynamic_jack_get_ports +#define jack_port_register dynamic_jack_port_register +#define jack_port_unregister dynamic_jack_port_unregister +#define jack_port_get_latency dynamic_jack_port_get_latency +#define jack_port_name dynamic_jack_port_name +#define jack_connect dynamic_jack_connect +#define jack_port_get_buffer dynamic_jack_port_get_buffer + +#endif +#endif + +AudioJACKTarget::AudioJACKTarget(AudioCallbackPlaySource *source) : + AudioCallbackPlayTarget(source), + m_client(0), + m_bufferSize(0), + m_sampleRate(0) +{ + char name[100]; + 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_xrun_callback(m_client, xrunStatic, this); + 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); +} + +int +AudioJACKTarget::xrunStatic(void *arg) +{ + return ((AudioJACKTarget *)arg)->xrun(); +} + +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); + } + + size_t received = 0; + + if (m_source) { + received = m_source->getSourceSamples(nframes, buffers); + } + + for (size_t ch = 0; ch < m_outputs.size(); ++ch) { + for (size_t i = received; 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; +} + +int +AudioJACKTarget::xrun() +{ + std::cerr << "AudioJACKTarget: xrun!" << std::endl; + if (m_source) m_source->audioProcessingOverload(); + return 0; +} + +#endif /* HAVE_JACK */ + diff --git a/audioio/AudioJACKTarget.h b/audioio/AudioJACKTarget.h new file mode 100644 index 0000000..ba8d74a --- /dev/null +++ b/audioio/AudioJACKTarget.h @@ -0,0 +1,60 @@ +/* -*- 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); + int xrun(); + + static int processStatic(jack_nframes_t, void *); + static int xrunStatic(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 --git a/audioio/AudioPortAudioTarget.cpp b/audioio/AudioPortAudioTarget.cpp new file mode 100644 index 0000000..af4e8b6 --- /dev/null +++ b/audioio/AudioPortAudioTarget.cpp @@ -0,0 +1,254 @@ +/* -*- 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; + +#ifdef DEBUG_AUDIO_PORT_AUDIO_TARGET +#ifdef HAVE_PORTAUDIO_V18 + std::cerr << "AudioPortAudioTarget: Initialising for PortAudio v18" << std::endl; +#else + std::cerr << "AudioPortAudioTarget: Initialising for PortAudio v19" << std::endl; +#endif +#endif + + err = Pa_Initialize(); + if (err != paNoError) { + std::cerr << "ERROR: AudioPortAudioTarget: Failed to initialize PortAudio: " << Pa_GetErrorText(err) << std::endl; + return; + } + + m_bufferSize = 1024; + m_sampleRate = 44100; + if (m_source && (m_source->getSourceSampleRate() != 0)) { + m_sampleRate = m_source->getSourceSampleRate(); + } + +#ifdef HAVE_PORTAUDIO_V18 + m_latency = Pa_GetMinNumBuffers(m_bufferSize, m_sampleRate) * m_bufferSize; +#endif + +#ifdef HAVE_PORTAUDIO_V18 + err = Pa_OpenDefaultStream(&m_stream, 0, 2, paFloat32, + m_sampleRate, m_bufferSize, 0, + processStatic, this); +#else + err = Pa_OpenDefaultStream(&m_stream, 0, 2, paFloat32, + m_sampleRate, m_bufferSize, + processStatic, this); +#endif + + if (err != paNoError) { + std::cerr << "ERROR: AudioPortAudioTarget: Failed to open PortAudio stream: " << Pa_GetErrorText(err) << std::endl; + m_stream = 0; + Pa_Terminate(); + return; + } + +#ifndef HAVE_PORTAUDIO_V18 + const PaStreamInfo *info = Pa_GetStreamInfo(m_stream); + m_latency = int(info->outputLatency * m_sampleRate + 0.001); +#endif + + std::cerr << "PortAudio latency = " << m_latency << " frames" << std::endl; + + err = Pa_StartStream(m_stream); + + if (err != paNoError) { + std::cerr << "ERROR: AudioPortAudioTarget: Failed to start PortAudio stream: " << Pa_GetErrorText(err) << 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); + } + +#ifdef DEBUG_PORT_AUDIO_TARGET + std::cerr << "AudioPortAudioTarget: initialised OK" << std::endl; +#endif +} + +AudioPortAudioTarget::~AudioPortAudioTarget() +{ + if (m_stream) { + PaError err; + err = Pa_CloseStream(m_stream); + if (err != paNoError) { + std::cerr << "ERROR: AudioPortAudioTarget: Failed to close PortAudio stream: " << Pa_GetErrorText(err) << std::endl; + } + err = Pa_Terminate(); + if (err != paNoError) { + std::cerr << "ERROR: AudioPortAudioTarget: Failed to terminate PortAudio: " << Pa_GetErrorText(err) << std::endl; + } + } +} + +bool +AudioPortAudioTarget::isOK() const +{ + return (m_stream != 0); +} + +#ifdef HAVE_PORTAUDIO_V18 +int +AudioPortAudioTarget::processStatic(void *input, void *output, + unsigned long nframes, + PaTimestamp outTime, void *data) +{ + return ((AudioPortAudioTarget *)data)->process(input, output, + nframes, outTime); +} +#else +int +AudioPortAudioTarget::processStatic(const void *input, void *output, + unsigned long nframes, + const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags flags, void *data) +{ + return ((AudioPortAudioTarget *)data)->process(input, output, + nframes, timeInfo, + flags); +} +#endif + +void +AudioPortAudioTarget::sourceModelReplaced() +{ + m_source->setTargetSampleRate(m_sampleRate); +} + +#ifdef HAVE_PORTAUDIO_V18 +int +AudioPortAudioTarget::process(void *inputBuffer, void *outputBuffer, + unsigned long nframes, + PaTimestamp) +#else +int +AudioPortAudioTarget::process(const void *, void *outputBuffer, + unsigned long nframes, + const PaStreamCallbackTimeInfo *, + PaStreamCallbackFlags) +#endif +{ +#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 || int(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]; + } + } + + size_t received = 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) { + if (i < received) { + output[i * 2 + ch] = tmpbuf[ch][i] * m_outputGain; + float sample = fabsf(output[i * 2 + ch]); + if (sample > peak) peak = sample; + } else { + output[i * 2 + ch] = 0; + } + } + + } else if (ch == 1 && sourceChannels == 1) { + + for (size_t i = 0; i < nframes; ++i) { + if (i < received) { + output[i * 2 + ch] = tmpbuf[0][i] * m_outputGain; + float sample = fabsf(output[i * 2 + ch]); + if (sample > peak) peak = sample; + } else { + output[i * 2 + ch] = 0; + } + } + + } 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; +} + +#endif /* HAVE_PORTAUDIO */ + diff --git a/audioio/AudioPortAudioTarget.h b/audioio/AudioPortAudioTarget.h new file mode 100644 index 0000000..4aa53dd --- /dev/null +++ b/audioio/AudioPortAudioTarget.h @@ -0,0 +1,78 @@ +/* -*- 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 + +// This code can be compiled for either PortAudio v18 or v19. +// PortAudio v19 is the default. If you want to use v18, define +// the preprocessor symbol HAVE_PORTAUDIO_v18. + +#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: +#ifdef HAVE_PORTAUDIO_V18 + + int process(void *input, void *output, unsigned long frames, + PaTimestamp outTime); + + static int processStatic(void *, void *, unsigned long, + PaTimestamp, void *); + + PortAudioStream *m_stream; + +#else + + int process(const void *input, void *output, unsigned long frames, + const PaStreamCallbackTimeInfo *timeInfo, + PaStreamCallbackFlags statusFlags); + + static int processStatic(const void *, void *, unsigned long, + const PaStreamCallbackTimeInfo *, + PaStreamCallbackFlags, void *); + + PaStream *m_stream; + +#endif + + int m_bufferSize; + int m_sampleRate; + int m_latency; +}; + +#endif /* HAVE_PORTAUDIO */ + +#endif + diff --git a/audioio/AudioTargetFactory.cpp b/audioio/AudioTargetFactory.cpp new file mode 100644 index 0000000..c472533 --- /dev/null +++ b/audioio/AudioTargetFactory.cpp @@ -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 --git a/audioio/AudioTargetFactory.h b/audioio/AudioTargetFactory.h new file mode 100644 index 0000000..d7cbe24 --- /dev/null +++ b/audioio/AudioTargetFactory.h @@ -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 --git a/audioio/PhaseVocoderTimeStretcher.cpp b/audioio/PhaseVocoderTimeStretcher.cpp new file mode 100644 index 0000000..0fa9c6e --- /dev/null +++ b/audioio/PhaseVocoderTimeStretcher.cpp @@ -0,0 +1,626 @@ +/* -*- 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 and QMUL. + + 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 "PhaseVocoderTimeStretcher.h" + +#include +#include + +#include + +//#define DEBUG_PHASE_VOCODER_TIME_STRETCHER 1 + +PhaseVocoderTimeStretcher::PhaseVocoderTimeStretcher(size_t sampleRate, + size_t channels, + float ratio, + bool sharpen, + size_t maxOutputBlockSize) : + m_sampleRate(sampleRate), + m_channels(channels), + m_maxOutputBlockSize(maxOutputBlockSize), + m_ratio(ratio), + m_sharpen(sharpen), + m_totalCount(0), + m_transientCount(0), + m_n2sum(0), + m_mutex(new QMutex()) +{ + initialise(); +} + +PhaseVocoderTimeStretcher::~PhaseVocoderTimeStretcher() +{ + std::cerr << "PhaseVocoderTimeStretcher::~PhaseVocoderTimeStretcher" << std::endl; + + cleanup(); + + delete m_mutex; +} + +void +PhaseVocoderTimeStretcher::initialise() +{ + std::cerr << "PhaseVocoderTimeStretcher::initialise" << std::endl; + + calculateParameters(); + + m_analysisWindow = new Window(HanningWindow, m_wlen); + m_synthesisWindow = new Window(HanningWindow, m_wlen); + + m_prevPhase = new float *[m_channels]; + m_prevAdjustedPhase = new float *[m_channels]; + + m_prevTransientMag = (float *)fftf_malloc(sizeof(float) * (m_wlen / 2 + 1)); + m_prevTransientScore = 0; + m_prevTransient = false; + + m_tempbuf = (float *)fftf_malloc(sizeof(float) * m_wlen); + + m_time = new float *[m_channels]; + m_freq = new fftf_complex *[m_channels]; + m_plan = new fftf_plan[m_channels]; + m_iplan = new fftf_plan[m_channels]; + + m_inbuf = new RingBuffer *[m_channels]; + m_outbuf = new RingBuffer *[m_channels]; + m_mashbuf = new float *[m_channels]; + + m_modulationbuf = (float *)fftf_malloc(sizeof(float) * m_wlen); + + for (size_t c = 0; c < m_channels; ++c) { + + m_prevPhase[c] = (float *)fftf_malloc(sizeof(float) * (m_wlen / 2 + 1)); + m_prevAdjustedPhase[c] = (float *)fftf_malloc(sizeof(float) * (m_wlen / 2 + 1)); + + m_time[c] = (float *)fftf_malloc(sizeof(float) * m_wlen); + m_freq[c] = (fftf_complex *)fftf_malloc(sizeof(fftf_complex) * + (m_wlen / 2 + 1)); + + m_plan[c] = fftf_plan_dft_r2c_1d(m_wlen, m_time[c], m_freq[c], FFTW_MEASURE); + m_iplan[c] = fftf_plan_dft_c2r_1d(m_wlen, m_freq[c], m_time[c], FFTW_MEASURE); + + m_outbuf[c] = new RingBuffer + ((m_maxOutputBlockSize + m_wlen) * 2); + m_inbuf[c] = new RingBuffer + (lrintf(m_outbuf[c]->getSize() / m_ratio) + m_wlen); + + std::cerr << "making inbuf size " << m_inbuf[c]->getSize() << " (outbuf size is " << m_outbuf[c]->getSize() << ", ratio " << m_ratio << ")" << std::endl; + + + m_mashbuf[c] = (float *)fftf_malloc(sizeof(float) * m_wlen); + + for (size_t i = 0; i < m_wlen; ++i) { + m_mashbuf[c][i] = 0.0; + } + + for (size_t i = 0; i <= m_wlen/2; ++i) { + m_prevPhase[c][i] = 0.0; + m_prevAdjustedPhase[c][i] = 0.0; + } + } + + for (size_t i = 0; i < m_wlen; ++i) { + m_modulationbuf[i] = 0.0; + } + + for (size_t i = 0; i <= m_wlen/2; ++i) { + m_prevTransientMag[i] = 0.0; + } +} + +void +PhaseVocoderTimeStretcher::calculateParameters() +{ + std::cerr << "PhaseVocoderTimeStretcher::calculateParameters" << std::endl; + + m_wlen = 1024; + + //!!! In transient sharpening mode, we need to pick the window + //length so as to be more or less fixed in audio duration (i.e. we + //need to exploit the sample rate) + + //!!! have to work out the relationship between wlen and transient + //threshold + + if (m_ratio < 1) { + if (m_ratio < 0.4) { + m_n1 = 1024; + m_wlen = 2048; + } else if (m_ratio < 0.8) { + m_n1 = 512; + } else { + m_n1 = 256; + } + if (shouldSharpen()) { + m_wlen = 2048; + } + m_n2 = lrintf(m_n1 * m_ratio); + } else { + if (m_ratio > 2) { + m_n2 = 512; + m_wlen = 4096; + } else if (m_ratio > 1.6) { + m_n2 = 384; + m_wlen = 2048; + } else { + m_n2 = 256; + } + if (shouldSharpen()) { + if (m_wlen < 2048) m_wlen = 2048; + } + m_n1 = lrintf(m_n2 / m_ratio); + if (m_n1 == 0) { + m_n1 = 1; + m_n2 = lrintf(m_ratio); + } + } + + m_transientThreshold = lrintf(m_wlen / 4.5); + + m_totalCount = 0; + m_transientCount = 0; + m_n2sum = 0; + + + std::cerr << "PhaseVocoderTimeStretcher: channels = " << m_channels + << ", ratio = " << m_ratio + << ", n1 = " << m_n1 << ", n2 = " << m_n2 << ", wlen = " + << m_wlen << ", max = " << m_maxOutputBlockSize << std::endl; +// << ", outbuflen = " << m_outbuf[0]->getSize() << std::endl; +} + +void +PhaseVocoderTimeStretcher::cleanup() +{ + std::cerr << "PhaseVocoderTimeStretcher::cleanup" << std::endl; + + for (size_t c = 0; c < m_channels; ++c) { + + fftf_destroy_plan(m_plan[c]); + fftf_destroy_plan(m_iplan[c]); + + fftf_free(m_time[c]); + fftf_free(m_freq[c]); + + fftf_free(m_mashbuf[c]); + fftf_free(m_prevPhase[c]); + fftf_free(m_prevAdjustedPhase[c]); + + delete m_inbuf[c]; + delete m_outbuf[c]; + } + + fftf_free(m_tempbuf); + fftf_free(m_modulationbuf); + fftf_free(m_prevTransientMag); + + delete[] m_prevPhase; + delete[] m_prevAdjustedPhase; + delete[] m_inbuf; + delete[] m_outbuf; + delete[] m_mashbuf; + delete[] m_time; + delete[] m_freq; + delete[] m_plan; + delete[] m_iplan; + + delete m_analysisWindow; + delete m_synthesisWindow; +} + +void +PhaseVocoderTimeStretcher::setRatio(float ratio) +{ + QMutexLocker locker(m_mutex); + + size_t formerWlen = m_wlen; + m_ratio = ratio; + + std::cerr << "PhaseVocoderTimeStretcher::setRatio: new ratio " << ratio + << std::endl; + + calculateParameters(); + + if (m_wlen == formerWlen) { + + // This is the only container whose size depends on m_ratio + + RingBuffer **newin = new RingBuffer *[m_channels]; + + size_t formerSize = m_inbuf[0]->getSize(); + size_t newSize = lrintf(m_outbuf[0]->getSize() / m_ratio) + m_wlen; + + std::cerr << "resizing inbuf from " << formerSize << " to " + << newSize << " (outbuf size is " << m_outbuf[0]->getSize() << ", ratio " << m_ratio << ")" << std::endl; + + if (formerSize != newSize) { + + size_t ready = m_inbuf[0]->getReadSpace(); + + for (size_t c = 0; c < m_channels; ++c) { + newin[c] = new RingBuffer(newSize); + } + + if (ready > 0) { + + size_t copy = std::min(ready, newSize); + float *tmp = new float[ready]; + + for (size_t c = 0; c < m_channels; ++c) { + m_inbuf[c]->read(tmp, ready); + newin[c]->write(tmp + ready - copy, copy); + } + + delete[] tmp; + } + + for (size_t c = 0; c < m_channels; ++c) { + delete m_inbuf[c]; + } + + delete[] m_inbuf; + m_inbuf = newin; + } + + } else { + + std::cerr << "wlen changed" << std::endl; + cleanup(); + initialise(); + } +} + +size_t +PhaseVocoderTimeStretcher::getProcessingLatency() const +{ + return getWindowSize() - getInputIncrement(); +} + +size_t +PhaseVocoderTimeStretcher::getRequiredInputSamples() const +{ + QMutexLocker locker(m_mutex); + + if (m_inbuf[0]->getReadSpace() >= m_wlen) return 0; + return m_wlen - m_inbuf[0]->getReadSpace(); +} + +void +PhaseVocoderTimeStretcher::putInput(float **input, size_t samples) +{ + QMutexLocker locker(m_mutex); + + // 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; + + while (consumed < samples) { + + size_t writable = m_inbuf[0]->getWriteSpace(); + writable = std::min(writable, samples - consumed); + + if (writable == 0) { +#ifdef DEBUG_PHASE_VOCODER_TIME_STRETCHER + std::cerr << "WARNING: PhaseVocoderTimeStretcher::putInput: writable == 0 (inbuf has " << m_inbuf[0]->getReadSpace() << " samples available for reading, space for " << m_inbuf[0]->getWriteSpace() << " more)" << std::endl; +#endif + if (m_inbuf[0]->getReadSpace() < m_wlen || + m_outbuf[0]->getWriteSpace() < m_n2) { + std::cerr << "WARNING: PhaseVocoderTimeStretcher::putInput: Inbuf has " << m_inbuf[0]->getReadSpace() << ", outbuf has space for " << m_outbuf[0]->getWriteSpace() << " (n2 = " << m_n2 << ", wlen = " << m_wlen << "), won't be able to process" << std::endl; + break; + } + } else { + +#ifdef DEBUG_PHASE_VOCODER_TIME_STRETCHER + std::cerr << "writing " << writable << " from index " << consumed << " to inbuf, consumed will be " << consumed + writable << std::endl; +#endif + + for (size_t c = 0; c < m_channels; ++c) { + m_inbuf[c]->write(input[c] + consumed, writable); + } + consumed += writable; + } + + while (m_inbuf[0]->getReadSpace() >= m_wlen && + m_outbuf[0]->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. + + for (size_t c = 0; c < m_channels; ++c) { + + size_t got = m_inbuf[c]->peek(m_tempbuf, m_wlen); + assert(got == m_wlen); + + analyseBlock(c, m_tempbuf); + } + + bool transient = false; + if (shouldSharpen()) transient = isTransient(); + + size_t n2 = m_n2; + + if (transient) { + n2 = m_n1; + } + + ++m_totalCount; + if (transient) ++m_transientCount; + m_n2sum += n2; + +// std::cerr << "ratio for last 10: " < 50 && m_transientCount < m_totalCount) { + + int fixed = lrintf(m_transientCount * m_n1); + + int idealTotal = lrintf(m_totalCount * m_n1 * m_ratio); + int idealSquashy = idealTotal - fixed; + + int squashyCount = m_totalCount - m_transientCount; + + n2 = lrintf(idealSquashy / squashyCount); + +#ifdef DEBUG_PHASE_VOCODER_TIME_STRETCHER + if (n2 != m_n2) { + std::cerr << m_n2 << " -> " << n2 << std::endl; + } +#endif + } + + for (size_t c = 0; c < m_channels; ++c) { + + synthesiseBlock(c, m_mashbuf[c], + c == 0 ? m_modulationbuf : 0, + m_prevTransient ? m_n1 : m_n2); + + +#ifdef DEBUG_PHASE_VOCODER_TIME_STRETCHER + std::cerr << "writing first " << m_n2 << " from mashbuf, skipping " << m_n1 << " on inbuf " << std::endl; +#endif + m_inbuf[c]->skip(m_n1); + + for (size_t i = 0; i < n2; ++i) { + if (m_modulationbuf[i] > 0.f) { + m_mashbuf[c][i] /= m_modulationbuf[i]; + } + } + + m_outbuf[c]->write(m_mashbuf[c], n2); + + for (size_t i = 0; i < m_wlen - n2; ++i) { + m_mashbuf[c][i] = m_mashbuf[c][i + n2]; + } + + for (size_t i = m_wlen - n2; i < m_wlen; ++i) { + m_mashbuf[c][i] = 0.0f; + } + } + + m_prevTransient = transient; + + for (size_t i = 0; i < m_wlen - n2; ++i) { + m_modulationbuf[i] = m_modulationbuf[i + n2]; + } + + for (size_t i = m_wlen - n2; i < m_wlen; ++i) { + m_modulationbuf[i] = 0.0f; + } + + if (!transient) m_n2 = n2; + } + + +#ifdef DEBUG_PHASE_VOCODER_TIME_STRETCHER + std::cerr << "loop ended: inbuf read space " << m_inbuf[0]->getReadSpace() << ", outbuf write space " << m_outbuf[0]->getWriteSpace() << std::endl; +#endif + } + +#ifdef DEBUG_PHASE_VOCODER_TIME_STRETCHER + std::cerr << "PhaseVocoderTimeStretcher::putInput returning" << std::endl; +#endif + +// std::cerr << "ratio: nominal: " << getRatio() << " actual: " +// << m_total2 << "/" << m_total1 << " = " << float(m_total2) / float(m_total1) << " ideal: " << m_ratio << std::endl; +} + +size_t +PhaseVocoderTimeStretcher::getAvailableOutputSamples() const +{ + QMutexLocker locker(m_mutex); + + return m_outbuf[0]->getReadSpace(); +} + +void +PhaseVocoderTimeStretcher::getOutput(float **output, size_t samples) +{ + QMutexLocker locker(m_mutex); + + if (m_outbuf[0]->getReadSpace() < samples) { + std::cerr << "WARNING: PhaseVocoderTimeStretcher::getOutput: not enough data (yet?) (" << m_outbuf[0]->getReadSpace() << " < " << samples << ")" << std::endl; + size_t fill = samples - m_outbuf[0]->getReadSpace(); + for (size_t c = 0; c < m_channels; ++c) { + for (size_t i = 0; i < fill; ++i) { + output[c][i] = 0.0; + } + m_outbuf[c]->read(output[c] + fill, m_outbuf[c]->getReadSpace()); + } + } else { +#ifdef DEBUG_PHASE_VOCODER_TIME_STRETCHER + std::cerr << "enough data - writing " << samples << " from outbuf" << std::endl; +#endif + for (size_t c = 0; c < m_channels; ++c) { + m_outbuf[c]->read(output[c], samples); + } + } + +#ifdef DEBUG_PHASE_VOCODER_TIME_STRETCHER + std::cerr << "PhaseVocoderTimeStretcher::getOutput returning" << std::endl; +#endif +} + +void +PhaseVocoderTimeStretcher::analyseBlock(size_t c, float *buf) +{ + size_t i; + + // buf contains m_wlen samples + +#ifdef DEBUG_PHASE_VOCODER_TIME_STRETCHER + std::cerr << "PhaseVocoderTimeStretcher::analyseBlock (channel " << c << ")" << std::endl; +#endif + + m_analysisWindow->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[c][i] = buf[i]; + } + + fftf_execute(m_plan[c]); // m_time -> m_freq +} + +bool +PhaseVocoderTimeStretcher::isTransient() +{ + int count = 0; + + for (size_t i = 0; i <= m_wlen/2; ++i) { + + float real = 0.f, imag = 0.f; + + for (size_t c = 0; c < m_channels; ++c) { + real += m_freq[c][i][0]; + imag += m_freq[c][i][1]; + } + + float sqrmag = (real * real + imag * imag); + + if (m_prevTransientMag[i] > 0.f) { + float diff = 10.f * log10f(sqrmag / m_prevTransientMag[i]); + if (diff > 3.f) ++count; + } + + m_prevTransientMag[i] = sqrmag; + } + + bool isTransient = false; + +// if (count > m_transientThreshold && +// count > m_prevTransientScore * 1.2) { + if (count > m_prevTransientScore && + count > m_transientThreshold && + count - m_prevTransientScore > int(m_wlen) / 20) { + isTransient = true; + + +// std::cerr << "isTransient (count = " << count << ", prev = " << m_prevTransientScore << ", diff = " << count - m_prevTransientScore << ", ratio = " << (m_totalCount > 0 ? (float (m_n2sum) / float(m_totalCount * m_n1)) : 1.f) << ", ideal = " << m_ratio << ")" << std::endl; +// } else { +// std::cerr << " !transient (count = " << count << ", prev = " << m_prevTransientScore << ", diff = " << count - m_prevTransientScore << ")" << std::endl; + } + + m_prevTransientScore = count; + + return isTransient; +} + +void +PhaseVocoderTimeStretcher::synthesiseBlock(size_t c, + float *out, + float *modulation, + size_t lastStep) +{ + bool unchanged = (lastStep == m_n1); + + for (size_t i = 0; i <= m_wlen/2; ++i) { + + float phase = princargf(atan2f(m_freq[c][i][1], m_freq[c][i][0])); + float adjustedPhase = phase; + + if (!unchanged) { + + float omega = (2 * M_PI * m_n1 * i) / m_wlen; + + float expectedPhase = m_prevPhase[c][i] + omega; + + float phaseError = princargf(phase - expectedPhase); + + float phaseIncrement = (omega + phaseError) / m_n1; + + adjustedPhase = m_prevAdjustedPhase[c][i] + + lastStep * phaseIncrement; + + float mag = sqrtf(m_freq[c][i][0] * m_freq[c][i][0] + + m_freq[c][i][1] * m_freq[c][i][1]); + + float real = mag * cosf(adjustedPhase); + float imag = mag * sinf(adjustedPhase); + m_freq[c][i][0] = real; + m_freq[c][i][1] = imag; + } + + m_prevPhase[c][i] = phase; + m_prevAdjustedPhase[c][i] = adjustedPhase; + } + + fftf_execute(m_iplan[c]); // m_freq -> m_time, inverse fft + + for (size_t i = 0; i < m_wlen/2; ++i) { + float temp = m_time[c][i]; + m_time[c][i] = m_time[c][i + m_wlen/2]; + m_time[c][i + m_wlen/2] = temp; + } + + for (size_t i = 0; i < m_wlen; ++i) { + m_time[c][i] = m_time[c][i] / m_wlen; + } + + m_synthesisWindow->cut(m_time[c]); + + for (size_t i = 0; i < m_wlen; ++i) { + out[i] += m_time[c][i]; + } + + if (modulation) { + + float area = m_analysisWindow->getArea(); + + for (size_t i = 0; i < m_wlen; ++i) { + float val = m_synthesisWindow->getValue(i); + modulation[i] += val * area; + } + } +} + + diff --git a/audioio/PhaseVocoderTimeStretcher.h b/audioio/PhaseVocoderTimeStretcher.h new file mode 100644 index 0000000..ca6efbf --- /dev/null +++ b/audioio/PhaseVocoderTimeStretcher.h @@ -0,0 +1,187 @@ +/* -*- 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 and QMUL. + + 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 _PHASE_VOCODER_TIME_STRETCHER_H_ +#define _PHASE_VOCODER_TIME_STRETCHER_H_ + +#include "base/Window.h" +#include "base/RingBuffer.h" + +#include "data/fft/FFTapi.h" + +#include + +/** + * A time stretcher that alters the performance speed of audio, + * preserving pitch. + * + * This is based on the straightforward phase vocoder with phase + * unwrapping (as in e.g. the DAFX book pp275-), with optional + * percussive transient detection to avoid smearing percussive notes + * and resynchronise phases, and adding a stream API for real-time + * use. Principles and methods from Chris Duxbury, AES 2002 and 2004 + * thesis; Emmanuel Ravelli, DAFX 2005; Dan Barry, ISSC 2005 on + * percussion detection; code by Chris Cannam. + */ + +class PhaseVocoderTimeStretcher +{ +public: + PhaseVocoderTimeStretcher(size_t sampleRate, + size_t channels, + float ratio, + bool sharpen, + size_t maxOutputBlockSize); + virtual ~PhaseVocoderTimeStretcher(); + + /** + * Return the number of samples that would need to be added via + * putInput in order to provoke the time stretcher into doing some + * time stretching and making more output samples available. + * This will be an estimate, if transient sharpening is on; the + * caller may need to do the put/get/test cycle more than once. + */ + size_t getRequiredInputSamples() const; + + /** + * Put (and possibly process) a given number of input samples. + * Number should usually equal the value returned from + * getRequiredInputSamples(). + */ + void putInput(float **input, size_t samples); + + /** + * Get the number of processed samples ready for reading. + */ + size_t getAvailableOutputSamples() const; + + /** + * Get some processed samples. + */ + void getOutput(float **output, size_t samples); + + //!!! and reset? + + /** + * Change the time stretch ratio. + */ + void setRatio(float ratio); + + /** + * Get the hop size for input. + */ + size_t getInputIncrement() const { return m_n1; } + + /** + * Get the hop size for output. + */ + size_t getOutputIncrement() const { return m_n2; } + + /** + * Get the window size for FFT processing. + */ + size_t getWindowSize() const { return m_wlen; } + + /** + * Get the stretch ratio. + */ + float getRatio() const { return float(m_n2) / float(m_n1); } + + /** + * Return whether this time stretcher will attempt to sharpen transients. + */ + bool getSharpening() const { return m_sharpen; } + + /** + * Return the number of channels for this time stretcher. + */ + size_t getChannelCount() const { return m_channels; } + + /** + * Get the latency added by the time stretcher, in sample frames. + * This will be exact if transient sharpening is off, or approximate + * if it is on. + */ + size_t getProcessingLatency() const; + +protected: + /** + * Process a single phase vocoder frame from "in" into + * m_freq[channel]. + */ + void analyseBlock(size_t channel, float *in); // into m_freq[channel] + + /** + * Examine m_freq[0..m_channels-1] and return whether a percussive + * transient is found. + */ + bool isTransient(); + + /** + * Resynthesise from m_freq[channel] adding in to "out", + * adjusting phases on the basis of a prior step size of lastStep. + * Also add the window shape in to the modulation array (if + * present) -- for use in ensuring the output has the correct + * magnitude afterwards. + */ + void synthesiseBlock(size_t channel, float *out, float *modulation, + size_t lastStep); + + void initialise(); + void calculateParameters(); + void cleanup(); + + bool shouldSharpen() { + return m_sharpen && (m_ratio > 0.25); + } + + size_t m_sampleRate; + size_t m_channels; + size_t m_maxOutputBlockSize; + float m_ratio; + bool m_sharpen; + size_t m_n1; + size_t m_n2; + size_t m_wlen; + Window *m_analysisWindow; + Window *m_synthesisWindow; + + int m_totalCount; + int m_transientCount; + int m_n2sum; + + float **m_prevPhase; + float **m_prevAdjustedPhase; + + float *m_prevTransientMag; + int m_prevTransientScore; + int m_transientThreshold; + bool m_prevTransient; + + float *m_tempbuf; + float **m_time; + fftf_complex **m_freq; + fftf_plan *m_plan; + fftf_plan *m_iplan; + + RingBuffer **m_inbuf; + RingBuffer **m_outbuf; + float **m_mashbuf; + float *m_modulationbuf; + + QMutex *m_mutex; +}; + +#endif diff --git a/audioio/PlaySpeedRangeMapper.cpp b/audioio/PlaySpeedRangeMapper.cpp new file mode 100644 index 0000000..24088a8 --- /dev/null +++ b/audioio/PlaySpeedRangeMapper.cpp @@ -0,0 +1,133 @@ +/* -*- 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 QMUL. + + 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 "PlaySpeedRangeMapper.h" + +#include +#include + +PlaySpeedRangeMapper::PlaySpeedRangeMapper(int minpos, int maxpos) : + m_minpos(minpos), + m_maxpos(maxpos) +{ +} + +int +PlaySpeedRangeMapper::getPositionForValue(float value) const +{ + // value is percent + float factor = getFactorForValue(value); + int position = getPositionForFactor(factor); + return position; +} + +int +PlaySpeedRangeMapper::getPositionForFactor(float factor) const +{ + bool slow = (factor > 1.0); + + if (!slow) factor = 1.0 / factor; + + int half = (m_maxpos + m_minpos) / 2; + + factor = sqrtf((factor - 1.0) * 1000.f); + int position = lrintf(((factor * (half - m_minpos)) / 100.0) + m_minpos); + + if (slow) { + position = half - position; + } else { + position = position + half; + } + +// std::cerr << "value = " << value << " slow = " << slow << " factor = " << factor << " position = " << position << std::endl; + + return position; +} + +float +PlaySpeedRangeMapper::getValueForPosition(int position) const +{ + float factor = getFactorForPosition(position); + float pc = getValueForFactor(factor); + return pc; +} + +float +PlaySpeedRangeMapper::getValueForFactor(float factor) const +{ + float pc; + if (factor < 1.0) pc = ((1.0 / factor) - 1.0) * 100.0; + else pc = (1.0 - factor) * 100.0; +// std::cerr << "position = " << position << " percent = " << pc << std::endl; + return pc; +} + +float +PlaySpeedRangeMapper::getFactorForValue(float value) const +{ + // value is percent + + float factor; + + if (value <= 0) { + factor = 1.0 - (value / 100.0); + } else { + factor = 1.0 / (1.0 + (value / 100.0)); + } + +// std::cerr << "value = " << value << " factor = " << factor << std::endl; + return factor; +} + +float +PlaySpeedRangeMapper::getFactorForPosition(int position) const +{ + bool slow = false; + + if (position < m_minpos) position = m_minpos; + if (position > m_maxpos) position = m_maxpos; + + int half = (m_maxpos + m_minpos) / 2; + + if (position < half) { + slow = true; + position = half - position; + } else { + position = position - half; + } + + // position is between min and half (inclusive) + + float factor; + + if (position == m_minpos) { + factor = 1.0; + } else { + factor = ((position - m_minpos) * 100.0) / (half - m_minpos); + factor = 1.0 + (factor * factor) / 1000.f; + } + + if (!slow) factor = 1.0 / factor; + +// std::cerr << "position = " << position << " slow = " << slow << " factor = " << factor << std::endl; + + return factor; +} + +QString +PlaySpeedRangeMapper::getUnit() const +{ + return "%"; +} diff --git a/audioio/PlaySpeedRangeMapper.h b/audioio/PlaySpeedRangeMapper.h new file mode 100644 index 0000000..865b6e6 --- /dev/null +++ b/audioio/PlaySpeedRangeMapper.h @@ -0,0 +1,43 @@ +/* -*- 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 QMUL. + + 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 _PLAY_SPEED_RANGE_MAPPER_H_ +#define _PLAY_SPEED_RANGE_MAPPER_H_ + +#include "base/RangeMapper.h" + +class PlaySpeedRangeMapper : public RangeMapper +{ +public: + PlaySpeedRangeMapper(int minpos, int maxpos); + + virtual int getPositionForValue(float value) const; + virtual float getValueForPosition(int position) const; + + int getPositionForFactor(float factor) const; + float getValueForFactor(float factor) const; + + float getFactorForPosition(int position) const; + float getFactorForValue(float value) const; + + virtual QString getUnit() const; + +protected: + int m_minpos; + int m_maxpos; +}; + + +#endif diff --git a/document/Document.cpp b/document/Document.cpp new file mode 100644 index 0000000..b1562a1 --- /dev/null +++ b/document/Document.cpp @@ -0,0 +1,947 @@ +/* -*- 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 and QMUL. + + 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 "data/model/WaveFileModel.h" +#include "data/model/WritableWaveFileModel.h" +#include "data/model/DenseThreeDimensionalModel.h" +#include "data/model/DenseTimeValueModel.h" +#include "layer/Layer.h" +#include "base/CommandHistory.h" +#include "base/Command.h" +#include "view/View.h" +#include "base/PlayParameterRepository.h" +#include "base/PlayParameters.h" +#include "transform/TransformFactory.h" +#include +#include + +#include "data/model/AggregateWaveModel.h" +#include "data/model/SparseTimeValueModel.h" +#include "data/model/AlignmentModel.h" + +//!!! still need to handle command history, documentRestored/documentModified + +Document::Document() : + m_mainModel(0) +{ + connect(this, SIGNAL(modelAboutToBeDeleted(Model *)), + TransformFactory::getInstance(), + SLOT(modelAboutToBeDeleted(Model *))); +} + +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, + TransformId transform) +{ + Layer *newLayer = createLayer(type); + if (!newLayer) return 0; + + newLayer->setObjectName(getUniqueLayerName + (TransformFactory::getInstance()-> + getTransformFriendlyName(transform))); + + return newLayer; +} + +Layer * +Document::createDerivedLayer(TransformId transform, + Model *inputModel, + const PluginTransform::ExecutionContext &context, + QString configurationXml) +{ + Model *newModel = addDerivedModel(transform, inputModel, + context, configurationXml); + if (!newModel) { + // error already printed to stderr by addDerivedModel + 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(); + +// std::cerr << "Document::setMainModel: inspecting model " +// << (model ? model->objectName().toStdString() : "(null)") << " in layer " +// << layer->objectName().toStdString() << std::endl; + + if (model == oldMainModel) { +// std::cerr << "... it uses the old main model, replacing" << std::endl; + 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) { + +// std::cerr << "... it uses a model derived from the old main model, regenerating" << std::endl; + + // This model was derived from the previous main + // model: regenerate it. + + TransformId transform = m_models[model].transform; + PluginTransform::ExecutionContext context = m_models[model].context; + + Model *replacementModel = + addDerivedModel(transform, + m_mainModel, + context, + 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 { + std::cerr << "Replacing model " << model << " (type " + << typeid(*model).name() << ") with model " + << replacementModel << " (type " + << typeid(*replacementModel).name() << ") in layer " + << layer << " (name " << layer->objectName().toStdString() << ")" + << std::endl; + RangeSummarisableTimeValueModel *rm = + dynamic_cast(replacementModel); + if (rm) { + std::cerr << "new model has " << rm->getChannelCount() << " channels " << std::endl; + } else { + std::cerr << "new model is not a RangeSummarisableTimeValueModel!" << std::endl; + } + 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(TransformId transform, + Model *inputModel, + const PluginTransform::ExecutionContext &context, + Model *outputModelToAdd, + QString configurationXml) +{ + if (m_models.find(outputModelToAdd) != m_models.end()) { + std::cerr << "WARNING: Document::addDerivedModel: Model already added" + << std::endl; + return; + } + +// std::cerr << "Document::addDerivedModel: source is " << inputModel << " \"" << inputModel->objectName().toStdString() << "\"" << std::endl; + + ModelRecord rec; + rec.source = inputModel; + rec.transform = transform; + rec.context = context; + rec.configurationXml = configurationXml; + rec.refcount = 0; + + outputModelToAdd->setSourceModel(inputModel); + + 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.refcount = 0; + + m_models[model] = rec; + + alignModel(model); + + emit modelAdded(model); +} + +Model * +Document::addDerivedModel(TransformId transform, + Model *inputModel, + const PluginTransform::ExecutionContext &context, + 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.context == context && + i->second.configurationXml == configurationXml) { + return i->first; + } + } + + model = TransformFactory::getInstance()->transform + (transform, inputModel, context, configurationXml); + + if (!model) { + std::cerr << "WARNING: Document::addDerivedModel: no output model for transform " << transform.toStdString() << std::endl; + } else { + addDerivedModel(transform, inputModel, context, 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 + << " (\"" << layer->objectName().toStdString() + << "\") wants to use unregistered model " << model + << ": register the layer's model before setting it!" + << std::endl; + return; + } + + Model *previousModel = layer->getModel(); + + if (previousModel == model) { + std::cerr << "WARNING: Document::setModel: Layer " << layer << " (\"" + << layer->objectName().toStdString() + << "\") is already set to model " + << model << " (\"" + << (model ? model->objectName().toStdString() : "(null)") + << "\")" << std::endl; + return; + } + + if (model && model != m_mainModel) { + m_models[model].refcount ++; + } + + if (model && previousModel) { + PlayParameterRepository::getInstance()->copyParameters + (previousModel, model); + } + + LayerFactory::getInstance()->setModel(layer, model); + + if (previousModel) { + releaseModel(previousModel); + } +} + +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 (\"" +// << layer->objectName().toStdString() +// << "\") 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; + } +} + +std::vector +Document::getTransformInputModels() +{ + std::vector models; + + if (!m_mainModel) return models; + + models.push_back(m_mainModel); + + //!!! This will pick up all models, including those that aren't visible... + + for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) { + + Model *model = i->first; + if (!model || model == m_mainModel) continue; + DenseTimeValueModel *dtvm = dynamic_cast(model); + + if (dtvm) { + models.push_back(dtvm); + } + } + + return models; +} + +void +Document::alignModel(Model *model) +{ + if (!m_mainModel || model == m_mainModel) return; + + RangeSummarisableTimeValueModel *rm = + dynamic_cast(model); + if (!rm) return; + + // This involves creating three new models: + + // 1. an AggregateWaveModel to provide the mixdowns of the main + // model and the new model in its two channels, as input to the + // MATCH plugin + + // 2. a SparseTimeValueModel, which is the model automatically + // created by FeatureExtractionPluginTransform when running the + // MATCH plugin (thus containing the alignment path) + + // 3. an AlignmentModel, which stores the path model and carries + // out alignment lookups on it. + + // The first two of these are provided as arguments to the + // constructor for the third, which takes responsibility for + // deleting them. The AlignmentModel, meanwhile, is passed to the + // new model we are aligning, which also takes responsibility for + // it. We should not have to delete any of these new models here. + + AggregateWaveModel::ChannelSpecList components; + + components.push_back(AggregateWaveModel::ModelChannelSpec + (m_mainModel, -1)); + + components.push_back(AggregateWaveModel::ModelChannelSpec + (rm, -1)); + + Model *aggregate = new AggregateWaveModel(components); + + TransformId id = "vamp:match-vamp-plugin:match:path"; + + TransformFactory *factory = TransformFactory::getInstance(); + + Model *transformOutput = factory->transform + (id, aggregate, + factory->getDefaultContextForTransform(id, aggregate), + ""); + + SparseTimeValueModel *path = dynamic_cast + (transformOutput); + + if (!path) { + std::cerr << "Document::alignModel: ERROR: Failed to create alignment path (no MATCH plugin?)" << std::endl; + delete transformOutput; + delete aggregate; + return; + } + + AlignmentModel *alignmentModel = new AlignmentModel + (m_mainModel, model, aggregate, path); + + rm->setAlignment(alignmentModel); +} + +void +Document::alignModels() +{ + for (ModelMap::iterator i = m_models.begin(); i != m_models.end(); ++i) { + alignModel(i->first); + } +} + +Document::AddLayerCommand::AddLayerCommand(Document *d, + View *view, + Layer *layer) : + m_d(d), + m_view(view), + m_layer(layer), + m_name(qApp->translate("AddLayerCommand", "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(qApp->translate("RemoveLayerCommand", "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\""); + } + + // Models that are not used in a layer that is in a view should + // not be written. Get our list of required models first. + + std::set used; + + for (LayerViewMap::const_iterator i = m_layerViewMap.begin(); + i != m_layerViewMap.end(); ++i) { + + if (i->first && !i->second.empty() && i->first->getModel()) { + used.insert(i->first->getModel()); + } + } + + for (ModelMap::const_iterator i = m_models.begin(); + i != m_models.end(); ++i) { + + const Model *model = i->first; + const ModelRecord &rec = i->second; + + if (used.find(model) == used.end()) continue; + + // We need an intelligent way to determine which models need + // to be streamed (i.e. have been edited, or are small) and + // which should not be (i.e. remain as generated by a + // transform, and are large). + // + // At the moment we can get away with deciding not to stream + // dense 3d models or writable wave file models, provided they + // were generated from a transform, because at the moment there + // is no way to edit those model types so it should be safe to + // regenerate them. That won't always work in future though. + // It would be particularly nice to be able to ask the user, + // as well as making an intelligent guess. + + bool writeModel = true; + bool haveDerivation = false; + + if (rec.source && rec.transform != "") { + haveDerivation = true; + } + + if (haveDerivation) { + if (dynamic_cast(model)) { + writeModel = false; + } else if (dynamic_cast(model)) { + writeModel = false; + } + } + + if (writeModel) { + i->first->toXml(out, indent + " "); + } + + if (haveDerivation) { + + //!!! stream the rest of the execution context in both directions (i.e. not just channel) + + out << indent; + out << QString(" first)) + .arg(rec.context.channel) + .arg(rec.context.domain) + .arg(rec.context.stepSize) + .arg(rec.context.blockSize) + .arg(int(rec.context.windowType)) + .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 --git a/document/Document.h b/document/Document.h new file mode 100644 index 0000000..ec238dc --- /dev/null +++ b/document/Document.h @@ -0,0 +1,318 @@ +/* -*- 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 and QMUL. + + 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 "transform/PluginTransform.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 WaveFileModel. + * + * - Any number of imported Model objects, which contain data without any + * requirement to remember where the data came from or how to + * regenerate it. + * + * - Any number of Model objects that were generated by a Transform + * such as FeatureExtractionPluginTransform. 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 Layer objects. 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, TransformId); + + /** + * Create and return a suitable layer for the given transform, + * running the transform and associating the resulting model with + * the new layer. + */ + Layer *createDerivedLayer(TransformId, + Model *inputModel, + const PluginTransform::ExecutionContext &context, + 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; } + + /** + * Get the main model (the source for playback sample rate, etc). + */ + const WaveFileModel *getMainModel() const { return m_mainModel; } + + std::vector getTransformInputModels(); + + /** + * Add a derived model associated with the given transform, + * running the transform and returning the resulting model. + */ + Model *addDerivedModel(TransformId transform, + Model *inputModel, + const PluginTransform::ExecutionContext &context, + QString configurationXml); + + /** + * Add a derived model associated with the given transform. This + * is necessary to register any derived model that was not created + * by the document using createDerivedModel or createDerivedLayer. + */ + void addDerivedModel(TransformId, + Model *inputModel, + const PluginTransform::ExecutionContext &context, + 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: + 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); + + /** + * If model is suitable for alignment, align it against the main + * model and store the alignment in the model. + */ + void alignModel(Model *); + + /** + * Realign all models if the main model has changed. Is this wise? + */ + void alignModels(); + + /* + * Every model that is in use by a layer in the document must be + * found in either m_mainModel or m_models. 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; + TransformId transform; + PluginTransform::ExecutionContext context; + 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 --git a/document/SVFileReader.cpp b/document/SVFileReader.cpp new file mode 100644 index 0000000..7aceb06 --- /dev/null +++ b/document/SVFileReader.cpp @@ -0,0 +1,1132 @@ +/* -*- 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 and QMUL. + + 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 "layer/Layer.h" +#include "view/View.h" +#include "base/PlayParameters.h" +#include "base/PlayParameterRepository.h" + +#include "data/fileio/AudioFileReaderFactory.h" +#include "data/fileio/FileFinder.h" +#include "data/fileio/RemoteFile.h" + +#include "data/model/WaveFileModel.h" +#include "data/model/EditableDenseThreeDimensionalModel.h" +#include "data/model/SparseOneDimensionalModel.h" +#include "data/model/SparseTimeValueModel.h" +#include "data/model/NoteModel.h" +#include "data/model/TextModel.h" + +#include "view/Pane.h" + +#include "Document.h" + +#include +#include +#include + +#include + +SVFileReader::SVFileReader(Document *document, + SVFileReaderPaneCallback &callback, + QString location) : + m_document(document), + m_paneCallback(callback), + m_location(location), + m_currentPane(0), + m_currentDataset(0), + m_currentDerivedModel(0), + m_currentDerivedModelId(-1), + m_currentPlayParameters(0), + m_datasetSeparator(" "), + m_inRow(false), + m_inLayer(false), + m_inView(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); + + } else if (name == "measurement") { + + ok = readMeasurement(attributes); + + } else { + std::cerr << "WARNING: SV-XML: Unexpected element \"" + << name.toLocal8Bit().data() << "\"" << std::endl; + } + + 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 (haveModel(i->second) && + 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) { + if (m_currentDerivedModel < 0) { + std::cerr << "WARNING: SV-XML: Bad derivation output model id " + << m_currentDerivedModelId << std::endl; + } else if (haveModel(m_currentDerivedModelId)) { + std::cerr << "WARNING: SV-XML: Derivation has existing model " + << m_currentDerivedModelId + << " as target, not regenerating" << std::endl; + } else { + m_currentDerivedModel = m_models[m_currentDerivedModelId] = + m_document->addDerivedModel(m_currentTransform, + m_currentTransformSource, + m_currentTransformContext, + m_currentTransformConfiguration); + } + } else { + m_document->addDerivedModel(m_currentTransform, + m_currentTransformSource, + m_currentTransformContext, + m_currentDerivedModel, + m_currentTransformConfiguration); + } + + m_addedModels.insert(m_currentDerivedModel); + m_currentDerivedModel = 0; + m_currentDerivedModelId = -1; + m_currentTransform = ""; + m_currentTransformConfiguration = ""; + + } else if (name == "row") { + m_inRow = false; + } else if (name == "layer") { + m_inLayer = 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 (haveModel(id)) { + std::cerr << "WARNING: SV-XML: Ignoring duplicate model id " << id + << std::endl; + return false; + } + + QString name = attributes.value("name"); + + std::cerr << "SVFileReader::readModel: model name \"" << name.toStdString() << "\"" << std::endl; + + READ_MANDATORY(int, sampleRate, toInt); + + QString type = attributes.value("type").trimmed(); + bool mainModel = (attributes.value("mainModel").trimmed() == "true"); + + if (type == "wavefile") { + + WaveFileModel *model = 0; + FileFinder *ff = FileFinder::getInstance(); + QString originalPath = attributes.value("file"); + QString path = ff->find(FileFinder::AudioFile, + originalPath, m_location); + QUrl url(path); + + if (RemoteFile::canHandleScheme(url)) { + + RemoteFile rf(url); + rf.wait(); + + if (rf.isOK()) { + + model = new WaveFileModel(rf.getLocalFilename(), path); + if (!model->isOK()) { + delete model; + model = 0; + //!!! and delete local file? + } + } + } else { + + model = new WaveFileModel(path); + if (!model->isOK()) { + delete model; + model = 0; + } + } + + if (!model) return false; + + model->setObjectName(name); + 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 wavefile + // models. + + if (dimensions == 3) { + + READ_MANDATORY(int, windowSize, toInt); + READ_MANDATORY(int, yBinCount, toInt); + + EditableDenseThreeDimensionalModel *model = + new EditableDenseThreeDimensionalModel + (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; + + model->setObjectName(name); + 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); + model->setObjectName(name); + 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); + + bool haveMinMax = true; + float minimum = attributes.value("minimum").trimmed().toFloat(&ok); + if (!ok) haveMinMax = false; + float maximum = attributes.value("maximum").trimmed().toFloat(&ok); + if (!ok) haveMinMax = false; + + 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); + model->setObjectName(name); + m_models[id] = model; + } else { + SparseTimeValueModel *model; + if (haveMinMax) { + model = new SparseTimeValueModel + (sampleRate, resolution, minimum, maximum, notifyOnAdd); + } else { + model = new SparseTimeValueModel + (sampleRate, resolution, notifyOnAdd); + } + model->setScaleUnits(units); + model->setObjectName(name); + m_models[id] = model; + } + } else { + NoteModel *model; + if (haveMinMax) { + model = new NoteModel + (sampleRate, resolution, minimum, maximum, notifyOnAdd); + } else { + model = new NoteModel + (sampleRate, resolution, notifyOnAdd); + } + model->setValueQuantization(valueQuantization); + model->setScaleUnits(units); + model->setObjectName(name); + 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); + 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" ? PlaybackScrollContinuous : + tracking == "page" ? PlaybackScrollPage + : PlaybackIgnore); + + // Then set these values + view->setCentreFrame(centre); + view->setZoomLevel(zoom); + + // 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 (haveModel(modelId)) { + 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) { + + QString visible = attributes.value("visible"); + bool dormant = (visible == "false"); + + // We need to do this both before and after adding the layer + // to the view -- we need it to be dormant if appropriate + // before it's actually added to the view so that any property + // box gets the right state when it's added, but the add layer + // command sets dormant to false because it assumes it may be + // restoring a previously dormant layer, so we need to set it + // again afterwards too. Hm + layer->setLayerDormant(m_currentPane, dormant); + + m_document->addLayerToView(m_currentPane, layer); + + layer->setLayerDormant(m_currentPane, dormant); + } + + m_currentLayer = layer; + m_inLayer = true; + + 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 (haveModel(modelId)) { + 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); + +// std::cerr << "SVFileReader::addPointToDataset: frame = " << frame << std::endl; + + SparseOneDimensionalModel *sodm = dynamic_cast + (m_currentDataset); + + if (sodm) { +// std::cerr << "Current dataset is a sparse one dimensional model" << std::endl; + QString label = attributes.value("label"); + sodm->addPoint(SparseOneDimensionalModel::Point(frame, label)); + return true; + } + + SparseTimeValueModel *stvm = dynamic_cast + (m_currentDataset); + + if (stvm) { +// std::cerr << "Current dataset is a sparse time-value model" << std::endl; + 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) { +// std::cerr << "Current dataset is a note model" << std::endl; + float value = 0.0; + value = attributes.value("value").trimmed().toFloat(&ok); + size_t duration = 0; + duration = attributes.value("duration").trimmed().toUInt(&ok); + QString label = attributes.value("label"); + nm->addPoint(NoteModel::Point(frame, value, duration, label)); + return ok; + } + + TextModel *tm = dynamic_cast(m_currentDataset); + + if (tm) { +// std::cerr << "Current dataset is a text model" << std::endl; + float height = 0.0; + height = attributes.value("height").trimmed().toFloat(&ok); + QString label = attributes.value("label"); +// std::cerr << "SVFileReader::addPointToDataset: TextModel: frame = " << frame << ", height = " << height << ", label = " << label.toStdString() << ", ok = " << ok << std::endl; + 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) +{ + EditableDenseThreeDimensionalModel *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) +{ + EditableDenseThreeDimensionalModel *dtdm = + dynamic_cast + (m_currentDataset); + + bool warned = false; + + if (dtdm) { + QStringList data = text.split(m_datasetSeparator); + + DenseThreeDimensionalModel::Column values; + + for (QStringList::iterator i = data.begin(); i != data.end(); ++i) { + + if (values.size() == dtdm->getHeight()) { + 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); + } + } + + dtdm->setColumn(m_rowNumber, 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 (haveModel(modelId)) { + m_currentDerivedModel = m_models[modelId]; + } else { + // we'll regenerate the model when the derivation element ends + m_currentDerivedModel = 0; + } + + m_currentDerivedModelId = modelId; + + int sourceId = 0; + bool sourceOk = false; + sourceId = attributes.value("source").trimmed().toInt(&sourceOk); + + if (sourceOk && haveModel(sourceId)) { + m_currentTransformSource = m_models[sourceId]; + } else { + m_currentTransformSource = m_document->getMainModel(); + } + + m_currentTransform = transform; + m_currentTransformConfiguration = ""; + + m_currentTransformContext = PluginTransform::ExecutionContext(); + + bool ok = false; + int channel = attributes.value("channel").trimmed().toInt(&ok); + if (ok) m_currentTransformContext.channel = channel; + + int domain = attributes.value("domain").trimmed().toInt(&ok); + if (ok) m_currentTransformContext.domain = Vamp::Plugin::InputDomain(domain); + + int stepSize = attributes.value("stepSize").trimmed().toInt(&ok); + if (ok) m_currentTransformContext.stepSize = stepSize; + + int blockSize = attributes.value("blockSize").trimmed().toInt(&ok); + if (ok) m_currentTransformContext.blockSize = blockSize; + + int windowType = attributes.value("windowType").trimmed().toInt(&ok); + if (ok) m_currentTransformContext.windowType = WindowType(windowType); + + 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 (haveModel(modelId)) { + + 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"); + 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_currentDerivedModelId < 0 && !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; +} + +bool +SVFileReader::readMeasurement(const QXmlAttributes &attributes) +{ + std::cerr << "SVFileReader::readMeasurement: inLayer " + << m_inLayer << ", layer " << m_currentLayer << std::endl; + + if (!m_inLayer) { + std::cerr << "WARNING: SV-XML: Measurement found outside layer" << std::endl; + return false; + } + + m_currentLayer->addMeasurementRect(attributes); + return true; +} + +SVFileReaderPaneCallback::~SVFileReaderPaneCallback() +{ +} + diff --git a/document/SVFileReader.h b/document/SVFileReader.h new file mode 100644 index 0000000..712433a --- /dev/null +++ b/document/SVFileReader.h @@ -0,0 +1,234 @@ +/* -*- 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 and QMUL. + + 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 "transform/PluginTransform.h" + +#include + +#include + +class Pane; +class Model; +class Document; +class PlayParameters; + +class SVFileReaderPaneCallback +{ +public: + virtual ~SVFileReaderPaneCallback(); + virtual Pane *addPane() = 0; + virtual void setWindowSize(int width, int height) = 0; + virtual void addSelection(int start, int end) = 0; +}; + +/** + SVFileReader loads Sonic Visualiser XML files. (The SV file + format is bzipped XML.) + + Some notes about the SV XML format follow. We're very lazy with + our XML: there's no schema or DTD, and we depend heavily on + elements being in a particular order. + +\verbatim + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +\endverbatim + */ + + +class SVFileReader : public QXmlDefaultHandler +{ +public: + SVFileReader(Document *document, + SVFileReaderPaneCallback &callback, + QString location = ""); // for audio file locate mechanism + 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 &); + bool readMeasurement(const QXmlAttributes &); + void addUnaddedModels(); + + bool haveModel(int id) { + return (m_models.find(id) != m_models.end()) && m_models[id]; + } + + Document *m_document; + SVFileReaderPaneCallback &m_paneCallback; + QString m_location; + Pane *m_currentPane; + std::map m_layers; + std::map m_models; + std::set m_addedModels; + std::map m_awaitingDatasets; // map dataset id -> model id + Layer *m_currentLayer; + Model *m_currentDataset; + Model *m_currentDerivedModel; + int m_currentDerivedModelId; + PlayParameters *m_currentPlayParameters; + QString m_currentTransform; + Model *m_currentTransformSource; + PluginTransform::ExecutionContext m_currentTransformContext; + QString m_currentTransformConfiguration; + QString m_datasetSeparator; + bool m_inRow; + bool m_inLayer; + bool m_inView; + bool m_inData; + bool m_inSelections; + int m_rowNumber; + QString m_errorString; + bool m_ok; +}; + +#endif diff --git a/i18n/sonic-visualiser_en_GB.qm b/i18n/sonic-visualiser_en_GB.qm new file mode 100644 index 0000000..dec70ac Binary files /dev/null and b/i18n/sonic-visualiser_en_GB.qm differ diff --git a/i18n/sonic-visualiser_en_GB.ts b/i18n/sonic-visualiser_en_GB.ts new file mode 100644 index 0000000..0f7c203 --- /dev/null +++ b/i18n/sonic-visualiser_en_GB.ts @@ -0,0 +1,29 @@ + + + QColorDialog + + Color + Colour + + + &Basic colors + &Basic colours + + + &Custom colors + &Custom colours + + + &Define Custom Colors >> + &Define Custom Colours >> + + + &Add to Custom Colors + &Add to Custom Colours + + + Select color + Select colour + + + diff --git a/i18n/sonic-visualiser_ru.qm b/i18n/sonic-visualiser_ru.qm new file mode 100644 index 0000000..cbcf86e Binary files /dev/null and b/i18n/sonic-visualiser_ru.qm differ diff --git a/i18n/sonic-visualiser_ru.ts b/i18n/sonic-visualiser_ru.ts new file mode 100644 index 0000000..086481c --- /dev/null +++ b/i18n/sonic-visualiser_ru.ts @@ -0,0 +1,5372 @@ + + + + AddLayerCommand + + + Add %1 Layer + Добавить слой %1 + + + + AudioDial + + + Enter new value + Введите новое значение + + + + %1: %2%3 + %1: %2%3 + + + + %2%3 + %2%3 + + + + New value for %1, from %2 to %3 %4: + Новое значение для «%1», от %2 до %3 %4: + + + + New value for %1, from %2 to %3: + Новое значение для «%1», от %2 до %3: + + + + Enter a new value from %1 to %2 %3: + Введите новое значение от %1 до %2 %3: + + + + Enter a new value from %1 to %2: + Введите новое значение от %1 до %2: + + + + BZipFileDevice + + + File is already open + Этот файл уже открыт + + + + Append mode not supported + Режим добавления в конец не поддерживается + + + + File access mode not specified + Способ доступа к файлам не указан + + + + Read and write modes both specified + Режимы чтения и записи определены + + + + Failed to open file for writing + Не удалось открыть файл для записи + + + + Failed to open bzip2 stream for writing + Не удалось открыть поток bzip2 для записи + + + + Failed to open file for reading + Не удалось открыть файл для чтения + + + + Failed to open bzip2 stream for reading + Не удалось открыть поток bzip2 для чтения + + + + Internal error (open for neither read nor write) + Внутренняя ошибка (не открывается ни на чтение, ни на запись) + + + + File not open + Файл не открыт + + + + bzip2 stream write close error + Ошибка закрытия записи в поток bzip2 + + + + bzip2 stream read close error + Ошибка закрытия чтения потока bzip2 + + + + Internal error (close for neither read nor write) + Внутренняя ошибка (не закрывается ни чтение, ни запись) + + + + bzip2 stream read error + Ошибка чтения потока bzip2 + + + + CSVFileWriter + + + Failed to open file %1 for writing + Не удалось открыть файл %1 для записи + + + + CSVFormatDialog + + + Select Data Format + Выберите формат данных + + + + +Please select the correct data format for this file. + + +Выберите правильный формат данных для этого файла. + + + + + Each row specifies: + Каждая строка определяет: + + + + A point in time + Точку во времени + + + + A value at a time + Значение во точке времени + + + + A set of values + Серию значений + + + + The first column contains: + Первый столбец содержит: + + + + Time, in seconds + Время, в секундах + + + + Time, in audio sample frames + Время, в выборках звуковых сэмплов + + + + Data (rows are consecutive in time) + Данные (строки последовательны во времени) + + + + Audio sample rate (Hz): + Частота сэмплирования звука (Гц): + + + + Frame increment between rows: + + + + + +Example data from file: + +Пример данных из файла: + + + + OK + ОК + + + + Cancel + Отменить + + + + Colour3DPlotLayer + + + Time:%1 - %2 +Bin:%3 +Value:%4 + Время:%1 - %2 +Bin:%3 +Значение:%4 + + + + Linear + Линейный + + + + dB + Дб + + + + <unknown> + <неизвестно> + + + + Colour + Цвет + + + + Scale + Масштаб + + + + Normalize Columns + Нормализовать столбцы + + + + Normalize Visible Area + Нормализовать видимую область + + + + Log + Логарифмический + + + + +/-1 + +/-1 + + + + ColourMapper + + + <unknown> + <неизвестно> + + + + Default + По умолчанию + + + + White on Black + Белое на чёрном + + + + Black on White + Чёрное на белом + + + + Red on Blue + Красное на синем + + + + Yellow on Black + Жёлтое на чёрном + + + + Blue on Black + Синее на чёрном + + + + Sunset + Закат + + + + Fruit Salad + Фруктовый салат + + + + Banded + В полоску + + + + Highlight + Подсветка + + + + CommandHistory + + + &Undo + &Отменить + + + + Ctrl+Z + Ctrl+Z + + + + Re&do + Ве&рнуть + + + + Ctrl+Shift+Z + Ctrl+Shift+Z + + + + Nothing to undo + Нет отменяемых действий + + + + Nothing to redo + Нет повторяемых действий + + + + &Undo %1 + &Отменить действие «%1» + + + + Re&do %1 + Повто&рить действие «%1» + + + + Undo the last editing operation + Отменить последнее действие правки + + + + Redo the last operation that was undone + Повторить последнее действие правки + + + + Document::AddLayerCommand + + + Add %1 Layer + Добавить %1 слой + + + + Document::RemoveLayerCommand + + + Delete %1 Layer + Удалить %1 слой + + + + FFTModel + + + %1 Hz + %1 Гц + + + + Fader + + + Level: Off + Уровень: выкл + + + + Level: %1%2.%3%4 dB + Уровень: %1%2.%3%4 Дб + + + + Enter new fader level + Ввведите новый уровень фейдера + + + + New fader level, from %1 to %2 dBFS: + Новый уровень фейдера, от %1 до %2 dBFS: + + + + FeatureExtractionPluginTransform + + + FFT cache failed + Не удалось кэшировать FFT + + + + Failed to create the FFT model for this transform. +There may be insufficient memory or disc space to continue. + Не удалось создать модель FFT для этого преобразования. +Вероятно, не хватает памяти или дискового пространства для продолжения. + + + + FileFinder + + + Select file + Выберите файл + + + + All files (*.*) + Все файлы (*.*) + + + + Select a session file + Выберите файл сессии + + + + Sonic Visualiser session files (*.sv) +All files (*.*) + Файлы сессий Sonic Visualiser (*.sv) +Все файлы (*.*) + + + + Audio files (%1) +All files (*.*) + Звуковые файлы (%1) +Все файлы (*.*) + + + + All supported files (%1) +Sonic Visualiser Layer XML files (*.svl) +Comma-separated data files (*.csv) +Space-separated .lab files (*.lab) +MIDI files (*.mid) +Text files (*.txt) +All files (*.*) + Все поддерживаемые типы файлов (%1) +XML-файлы слоёв Sonic Visualiser (*.svl) +Разделённые запятой файлы данных (*.csv) +Разделённые пробелом файлы .lab (*.lab) +MIDI-файлы (*.mid) +Текстовые файлы (*.txt) +Все файлы (*.*) + + + + All supported files (*.sv %1) +Sonic Visualiser session files (*.sv) +Audio files (%1) +All files (*.*) + Все поддерживаемые типы файлов (*.sv %1) +Файлы сессий Sonic Visualiser (*.sv) +Звуковые файлы (%1) +Все файлы (*.*) + + + + All supported files (*.sv %1 %2) +Sonic Visualiser session files (*.sv) +Audio files (%1) +Layer files (%2) +All files (*.*) + Все поддерживаемые типы файлов (*.sv %1 %2) +Файлы сессий Sonic Visualiser (*.sv) +Звуковые файлы (%1) +Файлы слоёв (%2) +Все файлы (*.*) + + + + File does not exist + Файл не существует + + + + File "%1" does not exist + Файл "%1" не существует + + + + File is not readable + Файл нечитаем + + + + File "%1" can not be read + Файл "%1" не может быть прочитан + + + + Directory selected + Выбран каталог + + + + File "%1" is a directory + Файл "%1" является каталогом + + + + Non-file selected + Выбран не файл + + + + Path "%1" is not a file + Путь "%1" не является файлом + + + + File is empty + Файл пуст + + + + File "%1" is empty + Файл "%1" пуст + + + + Select a file to export to + Выберите файл, в который экспортировать + + + + WAV audio files (*.wav) +All files (*.*) + Звуковые файлы WAV (*.wav) +Все файлы (*.*) + + + + Sonic Visualiser Layer XML files (*.svl) +Comma-separated data files (*.csv) +Text files (*.txt) +All files (*.*) + XML-файлы слоёв Sonic Visualiser (*.svl) +Разделённые запятой данные (*.csv) +Текстовые файлы (*.txt) +Все файлы (*.*) + + + + File exists + Такой файл уже существует + + + + The file "%1" already exists. +Do you want to overwrite it? + Файл "%1" уже существует. +Вы хотите перезаписать его? + + + + Audio file "%1" could not be opened. +Do you want to locate it? + Не удалось открыть звуковой файл "%1". +Вы хотите указать его расположение? + + + + File "%1" could not be opened. +Do you want to locate it? + Не удалось открыть файл "%1". +Вы хотите указать его расположение? + + + + Failed to open file + Не удалось открыть файл + + + + Locate file... + Указать файл... + + + + Use URL... + Использовать URL... + + + + Cancel + Отменить + + + + Use URL + Использовать URL + + + + Please enter the URL to use for this file: + Укажите URL, который будет использоваться для этого файла: + + + + Failed to open location + Не удалось открыть местоположение + + + + URL "%1" could not be opened + Не удалось открыть URL "%1" + + + + Portable Network Graphics files (*.png) +All files (*.*) + Файлы Portable Network Graphics (*.png) +Все файлы (*.*) + + + + ItemEditDialog + + + Timing + Тайминг + + + + Time: + Время: + + + + frames + выборок + + + + sec + с + + + + usec + мс + + + + Duration: + Длительность: + + + + Properties + Свойства + + + + Value: + Значение: + + + + Text: + Текст: + + + + OK + ОК + + + + Reset + Сбросить + + + + Cancel + Отменить + + + + Layer + + + Waveform + Волновая форма + + + + Spectrogram + Спектрограмма + + + + Ruler + Линейка + + + + Time Instants + Отметки времени + + + + Time Values + Значения времени + + + + Notes + Ноты + + + + Text + Текст + + + + Colour 3D Plot + Цветной 3D-график + + + + Layer + Слой + + + + Spectrum + Спектр + + + + Time Slice + + + + + LayerTreeModel + + + Layer + Слой + + + + Model + Модель + + + + ListInputDialog + + + OK + ОК + + + + Cancel + Отменить + + + + MIDIFileReader + + + Wrong length for long data in MIDI stream (%1, should be %2) + Неправильная длительность больших данных в потоке MIDI (%1, должно быть %2) + + + + Wrong length for int data in MIDI stream (%1, should be %2) + Неправильная длительность int-данных в потоке MIDI (%1, должно быть %2) + + + + getMIDIByte called but no MIDI file open + Функция getMIDIByte вызвана, но ни один файл MIDI не открыт + + + + End of MIDI file encountered while reading + При чтении обнаружен конец файла MIDI + + + + Attempt to get more bytes than expected on Track + Попытка получить из дорожки больше байтов, чем ожидалось + + + + Attempt to read past MIDI file end + Попытка прочитать файл MIDI после его окончания + + + + getMIDIBytes called but no MIDI file open + Функция getMIDIBytes вызвана, но ни один файл MIDI не открыт + + + + Attempt to get more bytes than available on Track (%1, only have %2) + Попытка получить из дорожки больше байтов, чем ожидалось (%1, а есть лишь %2) + + + + getNumberFromMIDIBytes called but no MIDI file open + Функция getNumberFromMIDIBytes вызвана, но ни один файл MIDI не открыт + + + + skipToNextTrack called but no MIDI file open + Функция skipToNextTrack вызвана, но ни один файл MIDI не открыт + + + + Invalid event code %1 found + Обнаружен некорректный код события %1 + + + + Running status used for first event in track + + + + + No notes in MIDI file + В MIDI-файле нет нот + + + + MIDI file "%1" has no notes in any track + Ни в одной дорожке MIDI-файла "%1" нет нот + + + + Merge all tracks + Объединить все дорожки + + + + Merge all non-percussion tracks + Объединить все неперкуссионные дорожки + + + + - uses GM percussion channel + — использует канал перкуссии GM + + + + Track %1 (%2)%3 + Дорожка %1 (%2)%3 + + + + Track %1 (untitled)%3 + Дорожка %1 (без имени)%3 + + + + Select track or tracks to import + Выберите одну или несколько дорожек для импорта + + + + You can only import this file as a single annotation layer, +but the file contains more than one track, +or notes on more than one channel. + +Please select the track or merged tracks you wish to import: + Вы можете импортировать этот файл как одиночный слой аннотаций, +но в файле больше одной дорожки, +либо есть ноте в более чем одном канале. + +Выберите, хотите ли вы при импорте выбрать какую-то одну +дорожку или же свести все дорожки в одну: + + + + %1 - vel %2 + %1 - vel %2 + + + + MainWindow + + + Sonic Visualiser + Sonic Visualiser + + + + &Layer + С&лой + + + + &File + &Файл + + + + File Toolbar + Панель файлов + + + + &New Session + &Создать сессию + + + + Ctrl+N + Ctrl+N + + + + Clear the current Sonic Visualiser session and start a new one + Очистить текущую сессию Sonic Visualiser и начать новую + + + + &Open Session... + &Открыть сессию... + + + + Ctrl+O + Ctrl+O + + + + Open a previously saved Sonic Visualiser session file + Открыть файл сохранённой ранее сессии Sonic Visualiser + + + + &Open... + О&ткрыть... + + + + Open a session file, audio file, or layer + Открыть файл сессии, звуковой файл или слой + + + + &Save Session + Сохр&анить сессию + + + + Ctrl+S + Ctrl+S + + + + Save the current session into a Sonic Visualiser session file + Сохранить в файл текущую сессию Sonic Visualiser + + + + Save Session &As... + Сохранить сессию &как... + + + + Save the current session into a new Sonic Visualiser session file + Сохранить текущую сессию Sonic Visualiser в файл с другим именем + + + + &Import Audio File... + &Импортировать звуковой файл... + + + + Ctrl+I + Ctrl+I + + + + Import an existing audio file + Импортировать существующий звуковой файл + + + + Import Secondary Audio File... + Импортировать второй звуковой файл... + + + + Ctrl+Shift+I + Ctrl+Shift+I + + + + Import an extra audio file as a separate layer + Импортировать ещё один звуковой файл в отдельный слой + + + + &Export Audio File... + Э&кспортировать звуковой файл... + + + + Export selection as an audio file + Экспортировать выделенное в звуковой файл + + + + Import Annotation &Layer... + И&мпортировать слой аннотаций... + + + + Ctrl+L + Ctrl+L + + + + Import layer data from an existing file + Импортировать данные слоя из существующего файла + + + + Export Annotation Layer... + Экспортировать слой аннотаций... + + + + Export layer data to a file + Экспортировать данные слоя в файл + + + + &Quit + Вы&йти + + + + Ctrl+Q + Ctrl+Q + + + + &Edit + &Правка + + + + Cu&t + &Вырезать + + + + Ctrl+X + Ctrl+X + + + + &Copy + С&копировать + + + + Ctrl+C + Ctrl+C + + + + &Paste + Вст&авить + + + + Ctrl+V + Ctrl+V + + + + &Delete Selected Items + &Удалить выбранное + + + + Del + Del + + + + Select &All + В&ыделить всё + + + + Ctrl+A + Ctrl+A + + + + Select &Visible Range + Вы&делить всё видимое + + + + Ctrl+Shift+A + Ctrl+Shift+A + + + + Select to &Start + Выделить до &начала + + + + Shift+Left + Shift+Left + + + + Select to &End + Выделить до &конца + + + + Shift+Right + Shift+Right + + + + C&lear Selection + Сн&ять выделение + + + + Esc + Esc + + + + &Insert Instant at Playback Position + Вставить &отметку в точку воспроизведения + + + + Enter + Enter + + + + &View + &Вид + + + + 0 + 0 + + + + 9 + 9 + + + + 8 + 8 + + + + Scroll &Left + Прокрутить в&лево + + + + Left + Влево + + + + Scroll the current pane to the left + Прокрутить активное окно влево + + + + Scroll &Right + Прокрутить в&право + + + + Right + Вправо + + + + Scroll the current pane to the right + Прокрутить активное окно вправо + + + + Ctrl+Left + Ctrl+Влево + + + + Scroll the current pane a big step to the left + Сделать большой шаг прокрутки влево + + + + Ctrl+Right + Ctrl+Вправо + + + + Scroll the current pane a big step to the right + Сделать большой шаг прокрутки вправо + + + + Zoom &In + При&близить + + + + Up + Вверх + + + + Increase the zoom level + Увеличить масштаб отображения + + + + Zoom &Out + &Отдалить + + + + Down + Вниз + + + + Decrease the zoom level + Уменьшить масштаб отображения + + + + Restore &Default Zoom + &Восстановить обычный масштаб + + + + Zoom to &Fit + &Уместить в окне + + + + Zoom to show the whole file + Увидеть весь файл + + + + &Pane + &Окно + + + + Add &New Pane + Добавить &новое окно + + + + Alt+N + Alt+н + + + + Add a new pane containing only a time ruler + Добавить новое окно, содержащее только линейку времени + + + + Add New %1 Layer + Добавить новый слой «%1» + + + + Add a new empty layer of type %1 + Добавить новый пустой слой типа «%1» + + + + Alt+T + + + + + Add &Waveform + Добавить &волновую форму + + + + Alt+W + Alt+в + + + + Add a new pane showing a waveform view + Добавить новое окно с видом волновой формы + + + + Add a new layer showing a waveform view + Добавить новый слой с видом волновой формы + + + + Add &Spectrogram + Добавить &спектограмму + + + + Alt+S + Alt+с + + + + Add a new pane showing a dB spectrogram + Добавить новую панель со спектрограммой в Дб + + + + Add &Melodic Range Spectrogram + Добавить спектограмму &мелодического диапазона + + + + Alt+M + Alt+м + + + + Add &Peak Frequency Spectrogram + Добавить спектограмму &пиковой частоты + + + + Alt+P + Alt+п + + + + Add a new pane showing a spectrogram set up for tracking frequencies + Добавить новое окно, отображающее спектрограмму для отслеживания частот + + + + Add a new layer showing a spectrogram set up for tracking frequencies + Добавить новый слой, отображающий спектрограмму для отслеживания частот + + + + &All Channels Mixed + Все &каналы сведены + + + + &All Channels + &Все каналы + + + + Channel &%1 + Канал &%1 + + + + &Delete Pane + У&далить окно + + + + Alt+D + Alt+д + + + + Delete the currently selected pane + Удалить активную панель + + + + Add &Time Ruler + Добавить линейку &времени + + + + Add a new layer showing a time ruler + Добавить новый слой с линейкой + + + + Add &Existing Layer + Добавить &существующий слой + + + + &Rename Layer... + &Переименовать слой... + + + + Alt+R + Alt+п + + + + Rename the currently active layer + Переименовать активный слой + + + + &Delete Layer + &Удалить слой + + + + Alt+Shift+D + Alt+Shift+D + + + + Delete the currently active layer + Удалить активный слой + + + + &Help + &Справка + + + + &Help Reference + &Справка по программе + + + + Open the Sonic Visualiser reference manual + Открыть справку по Sonic Visualiser + + + + Sonic Visualiser on the &Web + Sonic Visualiser в &Интернете + + + + Open the Sonic Visualiser website + Открыть веб-сайт Sonic Visualiser + + + + &About Sonic Visualiser + &О Sonic Visualiser + + + + Show information about Sonic Visualiser + Показать информацию о Sonic Visualiser + + + + Transport Toolbar + Панель транспорта + + + + Rewind to Start + Перемотать в начало + + + + Home + Домой + + + + Rewind to the start + Перемотать в начало + + + + Rewind + Назад + + + + PageUp + PageUp + + + + Rewind to the previous time instant in the current layer + Перемотать до предыдущей отметки времени в текущем слое + + + + Play / Pause + Воспроизвести / Приостановить + + + + Space + Пробел + + + + Start or stop playback from the current position + Запустить или остановить воспроизведение с текущей позиции + + + + Fast Forward + Вперёд + + + + PageDown + PageDown + + + + Fast forward to the next time instant in the current layer + Перемотать до следующей отметки времени в текущем слое + + + + Fast Forward to End + Перемотать до конца + + + + End + Конец + + + + Fast-forward to the end + Перемотать до конца + + + + Play Mode Toolbar + Панель режима воспроизведения + + + + Constrain Playback to Selection + Ограничить воспроизведение выделением + + + + s + с + + + + Constrain playback to the selected area + Ограничить воспроизведение выделенной областью + + + + Loop Playback + Воспроизведение в цикле + + + + l + l + + + + Loop playback + Воспроизведение в цикле + + + + Edit Toolbar + Панель правки + + + + Tools Toolbar + Панель инструментов + + + + Navigate + Перемещение + + + + 1 + 1 + + + + Select + Выделение + + + + 2 + 2 + + + + Edit + Правка + + + + 3 + 3 + + + + Draw + Рисование + + + + 4 + 4 + + + + No audio file loaded. + Ни один звуковой файл не загружен. + + + + %1Hz (resampling to %2Hz) + %1 Гц (ресэмплирование до %2 Гц) + + + + %1 (modified) + %1 (изменён) + + + + (modified) + (изменён) + + + + Cut + Вырезать + + + + Add Point + Добавить точку + + + + Add Points + Добавить точки + + + + Select an audio file + Выбрать звуковой файл + + + + Audio files (%1) +All files (*.*) + Звуковые файлы (%1) +Все файлы (*.*) + + + + Failed to open file + Не удалось открыть файл + + + + Audio file "%1" could not be opened + Не удалось открыть звуковой файл "%1" + + + + WAV audio files (*.wav) +All files (*.*) + Звуковые файлы WAV (*.wav) +Все файлы (*.*) + + + + Export the selected region only + Экспортировать только выделенную область + + + + Export the whole audio file + Экспортировать весь звуковой файл + + + + Select region to export + Выделите область для экспорта + + + + Which region from the original audio file do you want to export? + Какую область исходного звукового файла вы хотите экспортировать? + + + + Export the selected regions into a single audio file + Экспортировать выделенные области в один звуковой файл + + + + Export the selected regions into separate files + Экспортировать выделенные области в разные звуковые файлы + + + + Multiple regions of the original audio file are selected. +What do you want to export? + Выбрано несколько областей исходного звукового файла. +Какую из них вы хотите экспортировать? + + + + Fragment file %1 already exists, aborting + Файл фрагмента %1 уже существует, прерывание + + + + Failed to write file + Не удалось записать файл + + + + Select file + Выберите файл + + + + All supported files (%1) +Sonic Visualiser Layer XML files (*.svl) +Comma-separated data files (*.csv) +Space-separated .lab files (*.lab) +MIDI files (*.mid) +Text files (*.txt) +All files (*.*) + Все поддерживаемые типы файлов (%1) +XML-файлы слоёв Sonic Visualiser (*.svl) +Разделённые запятой файлы данных (*.csv) +Разделённые пробелом файлы .lab (*.lab) +MIDI-файлы (*.mid) +Текстовые файлы (*.txt) +Все файлы (*.*) + + + + File %1 could not be opened. + Не удалось открыть файл %1. + + + + Sonic Visualiser Layer XML files (*.svl) +Comma-separated data files (*.csv) +Text files (*.txt) +All files (*.*) + XML-файлы слоёв Sonic Visualiser (*.svl) +Разделённые запятой данные (*.csv) +Текстовые файлы (*.txt) +Все файлы (*.*) + + + + Failed to open file %1 for writing + Не удалось открыть файл %1 для записи + + + + Replace the existing main waveform + Заменить существующую основную волновую форму + + + + Load this file into a new waveform pane + Загрузить этот файл в новое окно волновой формы + + + + Select target for import + Выбрать цель для импорта + + + + Sonic Visualiser: %1 + Sonic Visualiser: %1 + + + + Sonic Visualiser: %1 [%2] + Sonic Visualiser: %1 [%2] + + + + Import "%1" + Импортировать "%1" + + + + Couldn't open audio device + Не удалось открыть звуковое устройство + + + + Could not open an audio device for playback. +Audio playback will not be available during this session. + + Не удалось открыть звуковое устройство для воспроизведения. +В этот раз воспроизведение будет недоступно. + + + + + File "%1" does not exist or is not a readable file + Файл "%1" не существует, либо в него нельзя записывать + + + + Session file "%1" could not be opened + Не удалось открыть файл сессии "%1" + + + + All supported files (*.sv %1 %2) +Sonic Visualiser session files (*.sv) +Audio files (%1) +Layer files (%2) +All files (*.*) + All supported files (*.sv %1 %2) +Sonic Visualiser session files (*.sv) +Audio files (%1) +Layer files (%2) +All files (*.*) + + + + All supported files (*.sv %1) +Sonic Visualiser session files (*.sv) +Audio files (%1) +All files (*.*) + All supported files (*.sv %1) +Sonic Visualiser session files (*.sv) +Audio files (%1) +All files (*.*) + + + + Select a file to open + Выберите файл для открытия + + + + File "%1" could not be opened + Не удалось открыть файл "%1" + + + + SV XML file read error: +%1 + Ошибка чтения XML-файла SV: +%1 + + + + Session modified + Сессия изменена + + + + The current session has been modified. +Do you want to save it? + Текущая сессия была изменена. +Вы хотите сохранить её? + + + + Failed to save file + Не удалось сохранить файл + + + + Session file "%1" could not be saved. + Не удалось сохранить файл сессии "%1". + + + + Directory selected + Выбран каталог + + + + File "%1" is a directory + Файл "%1" является каталогом + + + + File exists + Такой файл уже существует + + + + The file "%1" already exists. +Do you want to overwrite it? + Файл "%1" уже существует. +Вы хотите перезаписать его? + + + + Failed to write to file "%1": %2 + Не удалось записать файл "%1": %2 + + + + Delete Pane + Удалить окно + + + + Rename Layer + Переименовать слой + + + + New name for this layer: + Новое имя этого слоя: + + + + Sample rate mismatch + Несоответствие частоты дискретизации + + + + Failed to regenerate layer + Не удалось повторно создать слой + + + + http://www.sonicvisualiser.org/ + http://www.sonicvisualiser.org/ + + + + http://www.sonicvisualiser.org/doc/reference/en/ + http://www.sonicvisualiser.org/doc/reference/en/ + + + + Release %1 : Revision %2 + Версия %1 : Редакция %2 + + + + Release %1 + Версия %1 + + + + Unreleased : Revision %1 + Не выпущено : редакция %1 + + + + <h3>About Sonic Visualiser</h3> + <h3>О программе Sonic Visualiser</h3> + + + + <p>%1 : %2 build</p> + <p>%1 : сборка %2</p> + + + + Debug + Отладка + + + + Release + Версия + + + + <p>Statically linked + <p>Статически слинкована + + + + <br>With Qt (v%1) &copy; Trolltech AS + <br>С Qt (v%1) &copy; Trolltech AS + + + + <br>With JACK audio output (v%1) &copy; Paul Davis and Jack O'Quin + <br>С выводом в JACK (v%1) &copy; Paul Davis и Jack O'Quin + + + + <br>With PortAudio audio output &copy; Ross Bencina and Phil Burk + <br>С выводом в PortAudio &copy; Ross Bencina и Phil Burk + + + + <br>With Ogg file decoder (oggz v%1, fishsound v%2) &copy; CSIRO Australia + <br>С декодером Ogg (oggz v%1, fishsound v%2) &copy; CSIRO Australia + + + + <br>With MAD mp3 decoder (v%1) &copy; Underbit Technologies Inc + <br>С декодером MAD mp3 (v%1) &copy; Underbit Technologies Inc + + + + <br>With libsamplerate (v%1) &copy; Erik de Castro Lopo + <br>С libsamplerate (v%1) &copy; Erik de Castro Lopo + + + + <br>With libsndfile (v%1) &copy; Erik de Castro Lopo + <br>С libsndfile (v%1) &copy; Erik de Castro Lopo + + + + <br>With FFTW3 (v%1) &copy; Matteo Frigo and MIT + <br>С FFTW3 (v%1) &copy; Matteo Frigo и MIT + + + + <br>With Vamp plugin support (API v%1, SDK v%2) &copy; Chris Cannam + <br>С поддержкой расширений Vamp (API v%1, SDK v%2) &copy; Chris Cannam + + + + <br>With LADSPA plugin support (API v%1) &copy; Richard Furse, Paul Davis, Stefan Westerfeld + <br>С поддержкой расширений LADSPA (API v%1) &copy; Richard Furse, Paul Davis, Stefan Westerfeld + + + + <br>With DSSI plugin support (API v%1) &copy; Chris Cannam, Steve Harris, Sean Bolton + <br>С поддержкой расширений DSSI (API v%1) &copy; Chris Cannam, Steve Harris, Sean Bolton + + + + About Sonic Visualiser + О программе Sonic Visualiser + + + + Failed to generate layer + Не удалось создать слой + + + + Adjust the application preferences + Изменить параметры работы приложения + + + + You already have an audio waveform loaded. +What would you like to do with the new audio file? + У вас уже есть загруженная волновая форма звука. +Что вы хотите сделать с новым звуковым файлом? + + + + Sharpen percussive transients + Обострить перкуссионные моменты + + + + Run time stretcher in mono only + Запускать растягиватель времени только в монорежиме + + + + &Recent Files + Н&едавние файлы + + + + &Preferences... + &Параметры... + + + + ; + ; + + + + Show &Zoom Wheels + Показывать &колёса масштабирования + + + + Z + Z + + + + Show thumbwheels for zooming horizontally and vertically + Показывать колёса для масштабирования по горизонтали и вертикали + + + + %1 by Category + %1 по категории + + + + Unclassified + Неклассифицированные + + + + %1 by Maker + %1 по имени создателя + + + + Unknown + Неизвестен + + + + %1 by Plugin Name + %1 названию расширения + + + + %1... + %1... + + + + Add Spectr&um + Добавить &график спектральной функции + + + + Alt+U + Alt+г + + + + Add a new pane showing a frequency spectrum + Добавить новое окно с частотой спектра + + + + Add a new layer showing a frequency spectrum + Добавить новый слой с частотой спектра + + + + Playback Speedup + Ускорение воспроизведения + + + + &Transform + Пр&еобразования + + + + %1: %2 + %1: %2 + + + + &Recent Transforms + &Недавние преобразования + + + + [\(<].*$ + [\(<].*$ + + + + Audio processing overload + Перегрузка в обработке звука + + + + Audio effects plugin auditioning has been disabled +due to a processing overload. + Прослушивание обработки расширениями эффектов +отключено из-за перегрузки в обработке звука. + + + + Failed to generate a derived layer. + +The layer transform "%1" failed. + +This probably means that a plugin failed to initialise, perhaps because it +rejected the processing block size that was requested. + + + + + Failed to regenerate derived layer "%1". + +The layer transform "%2" failed to run. + +This probably means the layer used a plugin that is not currently available. + + + + + Show Property Bo&xes + Показывать панели с&войств + + + + X + + + + + Show the layer property boxes at the side of the main window + Показывать панели свойств слоёв сбоку от основного окна + + + + Add %1 Pane + Добавить окно %1 + + + + <br>With liblo Lite OSC library (v%1) &copy; Steve Harris + <br>С библиотекой liblo Lite OSC library (v%1) &copy; Steve Harris + + + + <p>The OSC URL for this instance is: "%1" + <p>OSC URL для этой копии процесса: "%1" + + + + Abandon the current Sonic Visualiser session and start a new one + Отказаться от текущей сессии Sonic Visualiser и начать новую + + + + Open Lo&cation... + Открыть &местоположение... + + + + Ctrl+Shift+O + Ctrl+Shift+O + + + + Open or import a file from a remote URL + Открыть или импортировать файл с удалённого узла + + + + Exit Sonic Visualiser + Выйти из Sonic Visualiser + + + + Cut the selection from the current layer to the clipboard + Вырезать выделение из активного слоя в буфер обмена + + + + Copy the selection from the current layer to the clipboard + Скопировать выделение из активного слоя в буфер обмена + + + + Paste from the clipboard to the current layer + Вставить содержимое буфера обмена в активный слой + + + + Delete the selection from the current layer + Удалить выделение из активного слоя + + + + Select the whole duration of the current session + Выбрать всю длительность активной сессии + + + + Select the time range corresponding to the current window width + Сделать временной диапазон равным активной ширине окна + + + + Select from the start of the session to the current playback position + Выделить от начала сессии до текущей точки воспроизведения + + + + Select from the current playback position to the end of the session + Выделить от текущей точки воспроизведения до конца сессии + + + + Clear the selection + Очистить выделение + + + + Insert a new time instant at the current playback position, in a new layer if necessary + Вставить новую отметку времени в точку воспроизведения, при необходимости — в новый слой + + + + Insert Instants at Selection &Boundaries + Вставить отметки времени по &краям выделения + + + + Shift+Enter + Shift+Enter + + + + Insert new time instants at the start and end of the current selection, in a new layer if necessary + Вставить новые отметки времени в начале и конце активного выделения, при необходимости — в новом слое + + + + &Jump Left + Перескочить в&лево + + + + J&ump Right + П&ерескочить вправо + + + + Restore the zoom level to the default + Восстановить обычный масштаб отображения + + + + Show &No Overlays + Не по&казывать перекрытия совсем + + + + Hide centre indicator, frame times, layer names and scale + Спрятать индикацию центра, времён выделения, имена слоёв и масштаб + + + + Show &Minimal Overlays + Показывать &минимум перекрытий + + + + Show centre indicator only + Показывать только индикатор центра + + + + Show &Standard Overlays + Показывать о&бычные перекрытия + + + + Show centre indicator, frame times and scale + Показывать индикатор центра, времена выделений, имена слоёв и масштаб + + + + Show &All Overlays + Показывать в&се перекрытия + + + + 7 + 7 + + + + Show all texts and scale + Показывать весь текст и масштаб + + + + Show Status &Bar + Показывать статусную &строку + + + + Show context help information in the status bar at the bottom of the window + Показывать контекстную справку в статусной строке, находящейся внизу окна программы + + + + Add a new pane showing a spectrogram + Добавить новое окно с отображением спектрограммы + + + + Add a new layer showing a spectrogram + Добавить новый слой с отображением спектрограммы + + + + Add a new pane showing a spectrogram set up for an overview of note pitches + Добавить новое окно, отображающее спектрограмму для обзора высоты тона нот + + + + Add a new layer showing a spectrogram set up for an overview of note pitches + Добавить новый слой, отображающий спектрограмму для обзора высоты тона нот + + + + Delete the currently active pane + Удалить активное окно + + + + Add S&lice of Layer + Добавить &фрагмент слоя + + + + Select ranges + Выбрать диапазон + + + + Edit items in layer + Изменить объекты слоя + + + + Draw new items in layer + Нарисовать новые объекты в слое + + + + Open Location + Открыть местоположение + + + + Please enter the URL of the location to open: + Введите URL открываемого местоположения: + + + + Failed to open location + Не удалось открыть местоположение + + + + URL "%1" could not be opened + Не удалось открыть URL "%1" + + + + Unsupported scheme in URL + Неподдерживаемая схема URL + + + + The URL scheme "%1" is not supported + Схема URL "%1" не поддерживается + + + + File download failed + Не удалось скачать файл + + + + Failed to download URL "%1": %2 + Не удалось скачать URL "%1": %2 + + + + Playing: %1 of %2 (%3 remaining) + Воспроизводится: %1 из %2 (осталось %3) + + + + Visible: %1 to %2 (duration %3) + Видимая область: от %1 до %2 (длительность %3) + + + + Adjust the master playback level + Изменить общую громкость воспроизведения + + + + Adjust the master playback speed + Изменить общую скорость воспроизведения + + + + Toggle transient sharpening for playback time scaling + + + + + Toggle mono mode for playback time scaling + Переключить моно-режим для масштабирования времени воспроизведения + + + + <p>Sonic Visualiser is a program for viewing and exploring audio data for<br>semantic music analysis and annotation.</p> + <p>Sonic Visualiser — программа для просмотра и исследования звуковых<br>данных с целью семантического анализа и аннотации музыки.</p> + + + + <p>%1 : %2 configuration</p> + <p>%1 : Конфигурация %2</p> + + + + <br>With JACK audio output &copy; Paul Davis and Jack O'Quin + <br>С выводом в JACK &copy; Paul Davis и Jack O'Quin + + + + <br>With Ogg file decoder &copy; CSIRO Australia + <br>С декодером Ogg &copy; CSIRO Australia + + + + <br>With MAD mp3 decoder &copy; Underbit Technologies Inc + <br>С декодером MAD mp3 &copy; Underbit Technologies Inc + + + + <br>With libsamplerate &copy; Erik de Castro Lopo + <br>С libsamplerate &copy; Erik de Castro Lopo + + + + <br>With libsndfile &copy; Erik de Castro Lopo + <br>С libsndfile &copy; Erik de Castro Lopo + + + + <br>With FFTW3 &copy; Matteo Frigo and MIT + <br>С FFTW3 &copy; Matteo Frigo и MIT + + + + <br>With Vamp plugin support (API v%1, host SDK v%2) &copy; Chris Cannam + <br>С поддержкой расширений Vamp (API v%1, host SDK v%2) &copy; Chris Cannam + + + + <br>With liblo Lite OSC library &copy; Steve Harris + <br>С liblo Lite OSC library &copy; Steve Harris + + + + Export Image File... + Экспортировать в файл изображения... + + + + Export a single pane to an image file + Экспортировать всё окно в файл изображения + + + + Export the whole pane (%1x%2 pixels) + Экспортировать всё окно (%1x%2 пикселов) + + + + Export the visible area only (%1x%2 pixels) + Экспортировать только видимую часть (%1x%2 пикселов) + + + + Export the selection extent (%1x%2 pixels) + Экспортировать выделение (%1x%2 пикселов) + + + + Export the selection extent + Экспортировать выделение + + + + Which region of the current pane do you want to export as an image? + Какую область текущего окна вы хотите экспортировать как изображение? + + + + Note: the whole pane is too wide to be exported as a single image. + Примечание: всё окно слишком широко, чтобы сохранить его как изображение. + + + + Failed to save image file + Не удалось сохранить файл изображения + + + + Failed to save image file %1 + Не удалось сохранить файл изображения %1 + + + + Selection: %1 to %2 (duration %3) + Выделение: %1 до %2 (длительность %3) + + + + The sample rate of this audio file (%1 Hz) does not match +the current playback rate (%2 Hz). + +The file will play at the wrong speed and pitch. + Частота сэмплирования этого звукового файла (%1 Гц) не совпадает +с текущей частотой сэмплирования для воспроизведения (%2 Гц). + +Файл будет воспроизведён с неправильной скоростью и высотой тона. + + + + http://www.sonicvisualiser.org/doc/reference/1.0/en/ + http://www.sonicvisualiser.org/doc/reference/1.0/ru/ + + + + <br>Using Qt v%1 &copy; Trolltech AS + <br>С Qt v%1 &copy; Trolltech AS + + + + MainWindow::AddPaneCommand + + + Add Pane + Добавить окно + + + + MainWindow::RemovePaneCommand + + + Remove Pane + Удалить окно + + + + NoteLayer + + + New Point + Новая точка + + + + Colour + Цвет + + + + Vertical Scale + Верт. масштаб + + + + Black + Чёрный + + + + Red + Красный + + + + Blue + Синий + + + + Green + Зелёный + + + + Purple + Пурпурный + + + + Orange + Оранжевый + + + + Auto-Align + Автовыравнивание + + + + Linear Scale + Линейная шкала + + + + Log Scale + Логарифм. шкала + + + + MIDI Note Range + Диапазон MIDI-нот + + + + <unknown> + <неизвестно> + + + + In progress + В процессе + + + + No local points + Нет локальных точек + + + + Time:%1 +Pitch:%2 +Duration:%3 +No label + Время:%1 +Высота тона:%2 +Длительность:%3 +Без метки + + + + Time:%1 +Pitch:%2 +Duration:%3 +Label:%4 + Время:%1 +Высота тона:%2 +Длительность:%3 +Метка:%4 + + + + Draw Point + Нарисовать точку + + + + Drag Point + Перетащить точку + + + + Edit Point + Изменить точку + + + + Relocate Point + Переместить точку + + + + Change Point Value + Изменить значение точки + + + + Drag Selection + Перетащить выделение + + + + Resize Selection + Изменить размер выделения + + + + Delete Selected Points + Удалить выбранные точки + + + + Paste + Вставить + + + + Scale Units + Единицы шкалы + + + + Scale + Масштаб + + + + Linear + Линейный + + + + Log + Логарифмический + + + + MIDI Notes + Ноты MIDI + + + + %1 (%2 Hz) + %1 (%2 Гц) + + + + %1 Hz (%2) + %1 Гц (%2) + + + + %1 %2 + %1 %2 + + + + Overview + + + Overview + Обзор + + + + Click and drag to navigate; double-click to jump + Щёлкните и потащите для перемещения по окну; щёлкните дважды для скачка + + + + Pane + + + Some lengthy prefix: + Некий длинный префикс: + + + + (R) + (R) + + + + (X) + (X) + + + + %1 / %2Hz%3 + %1 / %2 Гц%3 + + + + Drag Selection + Перетащить выделение + + + + Resize Selection + Изменить выделение + + + + Horizontal Zoom + Горизонтальное масштабирование + + + + Vertical Zoom + Вертикальное масштабирование + + + + Enter new range + Введите новый диапазон + + + + New vertical display range, from %1 to %2 %4: + Новый диапазон отображения по вертикали, от %1 до %2 %4: + + + + Click and drag to navigate + Щёлкните и потащите для перемещения по окну + + + + Click and drag to select a range; hold Shift to avoid snapping to items; hold Ctrl for multi-select; middle-click and drag to navigate + Щёлкните и потащите для выделения области; + Shift — для отключения прилипания к объектам; + Ctrl — для выделения нескольких объектов, + средняя клавиша мыши — для перемещения по окну + + + + Click and drag to select a range; hold Ctrl for multi-select; middle-click and drag to navigate + Щёлкните и потащите для выделения диапазона; + Ctrl — для выделения нескольких объектов, + средняя клавиша мыши — для перемещения по окну + + + + Click and drag to move the selection boundary + Щёлкните и потащите для перемещения границ выделения + + + + Click and drag to select a range; hold Shift to avoid snapping to items; middle-click to navigate + Щёлкните и потащите для выделения области; + Shift — для отключения прилипания к объектам; + средняя клавиша мыши — для перемещения по окну + + + + Click and drag to select a range; middle-click and drag to navigate + Щёлкните и потащите для выделения области; + средняя клавиша мыши — для перемещения по окну + + + + Click to add a new item in the active layer + Щёлкните для добавления нового объекта на активный слой + + + + Click and drag an item in the active layer to move it + Щёлкните объект и потащите его для перемещения в активном слое + + + + Click and drag to move all items in the selected range + Щёлкните и потащите все объекты в выделенной области + + + + Click and drag to adjust the visible range of the vertical scale + Щёлкните и потащите для смены видимого диапазона масштабирования по вертикали + + + + Click and drag to adjust the vertical zoom level + Щёлкните и потащите для смены уровня масштабирования по вертикали + + + + Click and drag to adjust the horizontal zoom level + Щёлкните и потащите для смены уровня масштабирования по горизонтали + + + + Reset horizontal and vertical zoom levels to their defaults + Сбросить уровни масштабирования по горизонтали и вертикали до исходных значений + + + + PluginParameterBox + + + This plugin has no adjustable parameters. + У этого расширения нет изменяемых параметров. + + + + Program + Программа + + + + PluginParameterDialog + + + Plugin + Расширение + + + + Name: + Имя: + + + + Type: + Тип: + + + + Maker: + Автор: + + + + Copyright: + Авторские права: + + + + Version: + Версия: + + + + Plugin Parameters + Параметры расширения + + + + Channel mismatch + Несовпадение каналов + + + + This plugin requires at least %1 input channels, but only %2 %3 available. The plugin probably will not work correctly. + Этому расширению нужно хотя бы %1 вхдных каналовs, но лишь %2 %3 доступны. Вероятно, расширение не сработает как должно. + + + + are + являются + + + + is + является + + + + Channels + Каналы + + + + This plugin accepts no more than %1 input channels, +but %2 are available. Only the first %3 will be used. + + Это расширение принимает не более %1 каналов на входе, +но доступно целых %2. Только первые %3 будут использованы. + + + + + This plugin only has a single channel input, +but the source has %1 channels. + Это расширение может обрабатывать только один канал, +но у источника %1 каналов. + + + + Use mean of source channels + Использовать среднее значение каналов источника + + + + Use channel %1 only + Использовать только канал %1 + + + + OK + ОК + + + + Cancel + Отменить + + + + Output: + Выход: + + + + Processing + Обработка + + + + Window size: + Размер оконной функции: + + + + Audio frames per block: + Выборок звука на блок: + + + + Window increment: + Шаг: + + + + Window shape: + Форма: + + + + Advanced >> + Больше >> + + + + Advanced << + Меньше << + + + + Input Source + Источник + + + + Preferences + + + Frequency of concert A + Частота концерта A + + + + Property box layout + Внешний вид панели свойств + + + + Spectral analysis window shape + Форма оконной функции при спектральном анализе + + + + Show boxes for all panes + Показывать панели для всех окон + + + + Show box for current pane only + Показывать панель только для активного окна + + + + Rectangular + Прямоугольная + + + + Triangular + Треугольная + + + + Hamming + Хамминга + + + + Hanning + Ханнинга + + + + Blackman + Блэкмена + + + + Gaussian + Гауссова + + + + Parzen + Парзена + + + + Nuttall + Нутталла + + + + Blackman-Harris + Блэкмена-Харриса + + + + Preferences + Параметры + + + + Playback resampler type + Тип ресэмплирования при воспроизведении + + + + Fastest + Самый быстрый + + + + Standard + Обычный + + + + Highest quality + Наивысшего качества + + + + Spectrogram y-axis smoothing: + Сглаживание спектрограммы по оси Y: + + + + None - blocky but accurate + Никакого — ступеньками, но аккуратно + + + + Interpolate - fast but fuzzy + Интерполировать — быстро, но грязно + + + + Zero pad FFT - slow but clear + Zero pad FFT — медленно, но чисто + + + + PreferencesDialog + + + Application Preferences + Настройки программы + + + + Sonic Visualiser Application Preferences + Настройки программы Sonic Visualiser + + + + Apply + Применить + + + + %1: + %1: + + + + OK + ОК + + + + Cancel + Отменить + + + + PropertyBox + + + Show + Показать + + + + Play + Воспроизвести + + + + Playback Pan / Balance + Панорама/баланс при воспроизведении + + + + Playback Gain + Усиление воспроизведения + + + + dB + Дб + + + + (current value: %1%2) + (текущее значение: %1%2) + + + + (current value: %1) + (текущее значение: %1) + + + + Toggle Visibility of %1 + Переключить видимость %1 + + + + Toggle Playback of %1 + Переключить воспроизведение %1 + + + + Toggle %1 property of %2 + Переключить %1 свойство %2 + + + + Adjust %1 property of %2%3 + Скорректировать %1 свойство %2%3 + + + + PropertyContainer + + + yes + да + + + + on + вкл + + + + true + правда + + + + no + нет + + + + off + выкл + + + + false + ложь + + + + PropertyContainer::SetPropertyCommand + + + Set %1 Property + Установить свойство %1 + + + + PropertyStack + + + Click to change the current active layer + Щёлкните для смены активного слоя + + + + QApplication + + + FFT cache resize failed + Не удалось изменить размер кэша FFT + + + + Failed to create or resize an FFT model slice. +There may be insufficient memory or disc space to continue. + + + + + +Sonic Visualiser is a program for viewing and exploring audio data +for semantic music analysis and annotation. + +Usage: + + %1 [--no-audio] [--no-osc] [<file> ...] + + --no-audio: Do not attempt to open an audio output device + --no-osc: Do not provide an Open Sound Control port for remote control + <file>: One or more Sonic Visualiser (.sv) and audio files may be provided. + + +Sonic Visualiser — программа для просмотра и исследования звуковых данных +с целью семантического анализа и аннотации музыки. + +Использование: + + %1 [--no-audio] [--no-osc] [<файл> ...] + + --no-audio: не пытаться открыть устройство звукового вывода + --no-osc: не предоставлять порт Open Sound Control для удалённого управления + <файл>: можно указать один и более файлов Sonic Visualiser (.sv) и звуковых файлов. + + + + + QFile + + + File "%1" does not exist + Файл "%1" не существует + + + + Failed to open file "%1" + Не удалось открыть файл "%1" + + + + QFileDialog + + + Locate file "%1" + Найти файл "%1" + + + + Audio files (%1) +All files (*.*) + Звуковые файлы (%1) +Все файлы (*.*) + + + + QMessageBox + + + Failed to open file + Не удалось открыть файл + + + + Audio file "%1" could not be opened. +Locate it? + Не удалось открыть звуковой файл "%1". +Найти его? + + + + File "%1" could not be opened + Не удалось открыть файл "%1" + + + + QObject + + + Stop + Остановить + + + + Decoding %1... + Декодируется %1... + + + + RangeInputDialog + + + to + до + + + + OK + ОК + + + + Cancel + Отменить + + + + RemoteFile + + + Downloading %1... + Скачивается %1... + + + + Cancel + Отменить + + + + Failed to connect to FTP server + Не удалось соединиться с FTP-сервером + + + + Login failed + Не удалось авторизоваться + + + + Failed to change to correct directory + Не удалось поменять на корректный каталог + + + + FTP download aborted + Скачивание с FTP-сервера прервано + + + + Download cancelled + Скачивание отменено + + + + Failed to create local file %1 + Не удалось создать локальный файл %1 + + + + File contains no data! + Файл не содержит данных! + + + + RemoveLayerCommand + + + Delete %1 Layer + Удалить слой %1 + + + + SliceLayer + + + %1 - %2 + %1 - %2 + + + + Time:%1 - %2 +Range:%3 samples +Bin:%4 +%5 value:%6 + Время:%1 - %2 +Диапазон:%3 сэмплов +Bin:%4 +%5 значение:%6 + + + + First + + + + + Mean + Усреднённо + + + + Peak + Пик + + + + Time:%1 - %2 +Range:%3 samples + Время:%1 - %2 +Диапазон:%3 сэмпл(-ов) + + + + 0dB + 0 Дб + + + + -Inf + -Inf + + + + x10 + x10 + + + + Colour + Цвет + + + + Plot Type + Тип графика + + + + Scale + Масштаб + + + + Normalize + Нормализовать + + + + Gain + Усиление + + + + Sampling Mode + Режим сэмплирования + + + + Plot X Scale + + + + + Black + Чёрный + + + + Red + Красный + + + + Blue + Синий + + + + Green + Зелёный + + + + Purple + Пурпурный + + + + Orange + Оранжевый + + + + Linear + Линейный + + + + Meter + + + + + dB + Дб + + + + Any + Любые + + + + Lines + Линии + + + + Steps + Шаги + + + + Blocks + Блоки + + + + Colours + Цвета + + + + Linear Bins + + + + + Log Bins + + + + + Rev Log Bins + + + + + <unknown> + <неизвестно> + + + + SparseModel + + + Add Point + Добавить точку + + + + Delete Point + Удалить точку + + + + Re-Label Point + Переразметить точку + + + + SpectrogramLayer + + + Colour + Цвет + + + + Colour Scale + + + + + Window Size + Размер окна + + + + Normalize Columns + Нормализовать столбцы + + + + Bin Display + + + + + Threshold + Порог + + + + Gain + Усиление + + + + Colour Rotation + Вращение цвета + + + + Min Frequency + Мин. частота + + + + Max Frequency + Макс. частота + + + + Frequency Scale + + + + + Window + Окно + + + + Scale + Масштаб + + + + Default + По умолчанию + + + + White on Black + Белое на чёрном + + + + Black on White + Чёрное на белом + + + + Red on Blue + Красное на синем + + + + Yellow on Black + Жёлтое на чёрном + + + + Blue on Black + Синее на чёрном + + + + Fruit Salad + Фруктовый салат + + + + Linear + Линейный + + + + Meter + + + + + dB + Дб + + + + Phase + Фаза + + + + No min + + + + + 10 Hz + 10 Гц + + + + 20 Hz + 20 Гц + + + + 40 Hz + 40 Гц + + + + 100 Hz + 100 Гц + + + + 250 Hz + 250 Гц + + + + 500 Hz + 500 Гц + + + + 1 KHz + 1 КГц + + + + 4 KHz + 4 КГц + + + + 10 KHz + 10 КГц + + + + 1.5 KHz + 1,5 КГц + + + + 2 KHz + 2 КГц + + + + 6 KHz + 6 КГц + + + + 8 KHz + 8 КГц + + + + 12 KHz + 12 КГц + + + + 16 KHz + 16 КГц + + + + No max + + + + + Log + Логарифм. + + + + All Bins + + + + + Peak Bins + + + + + Frequencies + Частоты + + + + <unknown> + <неизвестно> + + + + Peak Frequency:%1 - %2 Hz + + Пиковая частота:%1 - %2 Гц + + + + + Peak Frequency:%1 Hz + + Пиковая частота:%1 Гц + + + + + Peak Pitch:%3 - %4 + + Пиковая высота тона:%3 - %4 + + + + + Peak Pitch:%2 + + Пиковая высота тона:%2 + + + + + Time:%1 - %2 + + Время:%1 - %2 + + + + + Time:%1 + + Время:%1 + + + + + %1Bin Frequency:%2 - %3 Hz +%4Bin Pitch:%5 - %6 + + + + + + %1Bin Frequency:%2 Hz +%3Bin Pitch:%4 + + + + + + -Inf + -Inf + + + + dB:%1 - %2 + Дб:%1 - %2 + + + + dB:%1 + Дб:%1 + + + + +Phase:%1 - %2 + +Фаза:%1 - %2 + + + + +Phase:%1 + +Фаза:%1 + + + + Window Overlap + Перекрытие окон + + + + Smoothing + + + + + None + Нет + + + + 25 % + 25 % + + + + 50 % + 50 % + + + + 75 % + 75 % + + + + 87.5 % + 87,5 % + + + + 93.75 % + 93,75 % + + + + Normalize Visible Area + Нормализовать видимую область + + + + Bins + + + + + dB^2 + Дб^2 + + + + FFT cache failed + Не удалось кэшировать FFT + + + + Failed to create the FFT model for this spectrogram. +There may be insufficient memory or disc space to continue. + Не удалось создать FFT-модель этой спектрограммы. +Вероятно, не хватает памяти или дискового порстранства для продолжения. + + + + dBV^2 + ДбВ^2 + + + + dBV + ДбВ + + + + 43Hz + 43 Гц + + + + %1Hz + %1 Гц + + + + SpectrumLayer + + + Colour + Цвет + + + + Scale + Масштаб + + + + Channels + Каналы + + + + Window Size + Размер окна + + + + Window Overlap + Перекрытие окон + + + + Normalize + Нормализовать + + + + Gain + Усиление + + + + Window + Окно + + + + Energy Scale + Масштаб энергии + + + + Black + Чёрный + + + + Red + Красный + + + + Blue + Синий + + + + Green + Зелёный + + + + Purple + Пурпурный + + + + Orange + Оранжевый + + + + Linear + Линейный + + + + dB + Дб + + + + Mean + Усреднённо + + + + None + Ничего + + + + 25 % + 25 % + + + + 50 % + 50 % + + + + 75 % + 75 % + + + + 87.5 % + 87,5 % + + + + 93.75 % + 93,75 % + + + + <unknown> + <неизвестно> + + + + %1 - %2 + %1 - %2 + + + + %1 - %2 Hz + %1 - %2 Гц + + + + %1 Hz + %1 Гц + + + + -Inf + -Inf + + + + %1 + %1 + + + + %1 +Bin:%2 (%3) +%4 value:%5 +dB:%6 + + + + + First + + + + + Peak + Пик + + + + %1 +Bin:%2 (%3) +Value:%4 +dB:%5 + + + + + SubdividingMenu + + + %1 - %2 + %1 - %2 + + + + TextLayer + + + Empty Label + Очистить метку + + + + Colour + Цвет + + + + Black + Чёрный + + + + Red + Красный + + + + Blue + Синий + + + + Green + Зелёный + + + + Purple + Пурпурный + + + + Orange + Оранжевый + + + + <unknown> + <неизвестно> + + + + <no text> + <без текста> + + + + In progress + В процессе + + + + Time:%1 +Height:%2 +Label:%3 + Время:%1 +Высота:%2 +Метка:%3 + + + + Enter label + Введите метку + + + + Please enter a new label: + Введите текст новой метки: + + + + Drag Label + Перетащите метку + + + + Move Label + Переместите метку + + + + Move Label Horizontally + Переместите по горизонтали + + + + Move Label Vertically + Переместите по вертикали + + + + Drag Selection + Перетащите выделение + + + + Resize Selection + Смените размер выделения + + + + Delete Selection + Удалите выделение + + + + Paste + Вставить + + + + New Point + Новая точка + + + + Thumbwheel + + + %1: %2%3 + %1: %2%3 + + + + %2%3 + %2%3 + + + + New value for %1, from %2 to %3 %4: + Новое значение для %1, от %2 до %3 %4: + + + + New value for %1, from %2 to %3: + Новое значение для %1, от %2 до %3: + + + + Enter a new value from %1 to %2 %3: + Введите новое значение от %1 до %2 %3: + + + + Enter a new value from %1 to %2: + Введите новое значение от %1 до %2: + + + + Enter new value + Введите новое значение + + + + TimeInstantLayer + + + New Point + Новая точка + + + + Colour + Цвет + + + + Plot Type + Тип графика + + + + Black + Чёрный + + + + Red + Красный + + + + Blue + Синий + + + + Green + Зелёный + + + + Purple + Пурпурный + + + + Orange + Оранжевый + + + + Instants + + + + + Segmentation + Сегментация + + + + <unknown> + <неизвестно> + + + + In progress + В процессе + + + + No local points + Нет локальных точек + + + + Time:%1 +No label + Время:%1 +Без метки + + + + Time:%1 +Label:%2 + Время:%1 +Метка:%2 + + + + Draw Point + Нарисовать точку + + + + Add Point at %1 s + Добавить точку в %1 с + + + + Drag Point + Перетащить точку + + + + Move Point to %1 s + Перетащить точку к %1 с + + + + Edit Point + Изменить точку + + + + Drag Selection + Перетащить выделение + + + + Resize Selection + Изменить размер выделения + + + + Delete Selection + Удалить выделение + + + + Paste + Вставить + + + + TimeRulerLayer + + + Colour + Цвет + + + + Black + Чёрный + + + + Red + Красный + + + + Blue + Синий + + + + Green + Зелёный + + + + Purple + Пурпурный + + + + Orange + Оранжевый + + + + <unknown> + <неизвестно> + + + + TimeValueLayer + + + New Point + Новая точка + + + + Colour + Цвет + + + + Plot Type + Тип графика + + + + Vertical Scale + Верт. масштаб + + + + Scale Units + Единицы шкалы + + + + Black + Чёрный + + + + Red + Красный + + + + Blue + Синий + + + + Green + Зелёный + + + + Purple + Пурпурный + + + + Orange + Оранжевый + + + + Points + Точки + + + + Stems + Ножки + + + + Connected Points + Соединённые точки + + + + Lines + Линии + + + + Curve + Кривая + + + + Segmentation + Сегментация + + + + Auto-Align + Автовыравнивание + + + + Linear Scale + Линейная шкала + + + + Log Scale + Логарифм. шкала + + + + <unknown> + <неизвестно> + + + + In progress + В процессе + + + + No local points + Нет локальных точек + + + + Time:%1 +Value:%2%3 +No label + Время:%1 +Значение:%2%3 +Без метки + + + + Time:%1 +Value:%2%3 +Label:%4 + Время:%1 +Значение:%2%3 +Метка:%4 + + + + Draw Point + Нарисовать точку + + + + Drag Point + Перетащить точку + + + + Edit Point + Изменить точку + + + + Relocate Point + Переместить точку + + + + Change Point Value + Изменить значение точки + + + + Drag Selection + Перетащить выделение + + + + Resize Selection + Изменить размер выделения + + + + Delete Selected Points + Удалить выбранные точки + + + + Paste + Вставить + + + + The items you are pasting do not have values. +What values do you want to use for these items? + У вставляемых объектов нет значений. +Какие значения вы хотите для них использовать? + + + + Some of the items you are pasting do not have values. +What values do you want to use for these items? + У некоторых вставляемых объектов нет значений.Какие значения вы хотите для них использовать? + + + + Zero for all items + Ноль для всех объектов + + + + Whole numbers counting from 1 + Целые числа начиная с 1 + + + + Item's audio sample frame number + Число звуковых выборок объекта + + + + Item's time in seconds + Время объекта в секундах + + + + Duration from the item to the following item + Длительность от одного объекта до следующего + + + + Tempo in bpm derived from the duration + Время в bpm, извлечённое из длительности + + + + Value of the nearest existing item + Значение ближайшего существующего объекта + + + + Value extracted from the item's label (where possible) + Значение, извлечённое из метки объекта (если возможно) + + + + Choose value calculation + Выбрать вычисление значения + + + + Scale + Масштаб + + + + Linear + Линейный + + + + Log + Логарифм. + + + + +/-1 + +/-1 + + + + TipDialog + + + Tip of the Day + Совет дня + + + + Show tip on startup + Показывать советы при запуске + + + + << Previous + << Предыдущий + + + + Next >> + Следующий >> + + + + Close + Закрыть + + + + TransformFactory + + + %1: %2 + %1: %2 + + + + %1: Output %2 + %1: Выход %2 + + + + Analysis + Анализ + + + + Effects Data + Данные эффектов + + + + Effects + Эффекты + + + + Generators + Генераторы + + + + %1 <%2> + %1 <%2> + + + + [\(<].*$ + [\(<].*$ + + + + <unknown maker> + <неизвестный производитель> + + + + Extract features using "%1" plugin (from %2) + + + + + Extract features using "%1" output of "%2" plugin (from %3) + + + + + %1 using "%2" plugin (from %3) + %1 используя расширение "%2" (из %3) + + + + %1 using "%2" output of "%3" plugin (from %4) + %1 используя вывод "%2" расширения "%3" (из %4) + + + + Extract "%1" data output from "%2" effect plugin (from %3) + Извлечь "%1" данные вывода из расширения эффектов "%2" (из %3) + + + + Extract data output %1 from "%2" effect plugin (from %3) + Извлечь данные вывода %1 из расширения эффектов "%2" (из %3) + + + + Transform audio signal with "%1" effect plugin (from %2) + Трансформировать звуковой сигнал расширением эффектов "%1" (из %2) + + + + Generate audio signal using "%1" plugin (from %2) + Создать звуковой сигнал при помощи расширения "%1" (из %2) + + + + View + + + Global Scroll + Глобальная прокрутка + + + + Global Zoom + Глобальный масштаб + + + + Follow Playback + Следовать за воспроизведением + + + + Scroll + Прокручивать + + + + Page + Постранично + + + + Off + Выключить + + + + <unknown> + <неизвестно> + + + + Waiting for layers to be ready... + Ожидание готовности слоёв... + + + + Cancel + Отменить + + + + Rendering image... + Отрисовывается изображение... + + + + ViewManager::SetSelectionCommand + + + Clear Selection + Снять выделение + + + + Select + Выделить + + + + WaveformLayer + + + Colour + Цвет + + + + Scale + Масштаб по Y + + + + Gain + Усиление + + + + Normalize Visible Area + Нормализовать видимую область + + + + Channels + Каналы + + + + Black + Чёрный + + + + Red + Красный + + + + Blue + Синий + + + + Green + Зелёный + + + + Purple + Пурпурный + + + + Orange + Оранжевый + + + + Linear + Линейный + + + + Meter + + + + + dB + Дб + + + + Separate + Отдельно + + + + Mean + Усреднённо + + + + Butterfly + V-образно + + + + <unknown> + <неизвестно> + + + + Time:%1 - %2 + Время:%1 - %2 + + + + Time:%1 + Время:%1 + + + + Level: + Уровень: + + + + Left: + Левый: + + + + Right: + Правый: + + + + Channel %1 + Канал %1 + + + + +%1%2 - %3 (%4 dB peak) + +%1%2 - %3 (пик в %4 Дб) + + + + +%1%2 (%3 dB peak) + +%1%2 (пик в %3 Дб) + + + + 0dB + 0 Дб + + + + -Inf + -Inf + + + + WindowShapePreview + + + V / time + V / время + + + + dB / freq + Дб / частота + + + diff --git a/i18n/tips_en.xml b/i18n/tips_en.xml new file mode 100644 index 0000000..065b11c --- /dev/null +++ b/i18n/tips_en.xml @@ -0,0 +1,73 @@ + + + + + + +

Welcome to Sonic Visualiser!

Sonic Visualiser is a +complex application, but it's easy enough to get started with.

+

Try importing an audio file and using the Pane menu to add some +different views of it.

Check out the links on the Help menu for +tutorials and other documentation!

+ +
+ + +

Sonic Visualiser's window is organised into "panes" and +"layers". To begin with, you have one pane. Import an audio +file to see it displayed in a waveform layer on that pane.

+ +
+ + +

Each pane can contain any number of layers, which appear stacked +from "front" to "back" on the same timeline. A layer can contain a +view of audio, or of points (time instants) or data plots (time-values).

+ +
+ + +

There's a "layer property box" to the right of each pane, with one +tab for each of the layers on that pane. Click on a layer's tab to +bring that layer to the front. You can then adjust its colour and +other display properties in the property box. + +The first tab is always for the pane itself, which has a few +adjustable properties of its own.

+ +
+ + +

If you have more than one pane, only one of them will be "current", +marked with a black bar at the left side. Just click on another pane +to make it current. Most editing operations affect the layer that's +at the front of the current pane.

+ +
+ + +

You can use different zoom levels for different panes by un-checking +the Global Zoom control for one of them. By default, all panes will +zoom and scroll together.

+ +
+ + +

You can speed up and slow down playback using the Playback Speedup +control in the bottom-right of the window.

+ +
+ + +

The Transforms menu lists things you can do to extract features from +or process your audio. The available outputs of any Vamp +feature-extraction plugins or LADSPA audio effects plugins you have +will show up here.

+ +
+ +
+ + diff --git a/icons/README b/icons/README new file mode 100644 index 0000000..42757c5 --- /dev/null +++ b/icons/README @@ -0,0 +1,3 @@ +Icons are from various GPL'd sources, including the GIMP image editor, +KDE crystalsvg icon set, Rosegarden audio and MIDI sequencer, and Hydrogen +drum machine. Some modifications and new drawings by Chris Cannam. diff --git a/icons/annotation.png b/icons/annotation.png new file mode 100644 index 0000000..3bfa627 Binary files /dev/null and b/icons/annotation.png differ diff --git a/icons/colour3d.png b/icons/colour3d.png new file mode 100644 index 0000000..9ec5dd9 Binary files /dev/null and b/icons/colour3d.png differ diff --git a/icons/draw-curve.png b/icons/draw-curve.png new file mode 100644 index 0000000..995049f Binary files /dev/null and b/icons/draw-curve.png differ diff --git a/icons/draw.png b/icons/draw.png new file mode 100644 index 0000000..01bf2c6 Binary files /dev/null and b/icons/draw.png differ diff --git a/icons/editcopy.png b/icons/editcopy.png new file mode 100644 index 0000000..c3ff243 Binary files /dev/null and b/icons/editcopy.png differ diff --git a/icons/editcut.png b/icons/editcut.png new file mode 100644 index 0000000..7ec355a Binary files /dev/null and b/icons/editcut.png differ diff --git a/icons/editdelete.png b/icons/editdelete.png new file mode 100644 index 0000000..6d0d29d Binary files /dev/null and b/icons/editdelete.png differ diff --git a/icons/editpaste.png b/icons/editpaste.png new file mode 100644 index 0000000..f6a1db8 Binary files /dev/null and b/icons/editpaste.png differ diff --git a/icons/exit.png b/icons/exit.png new file mode 100644 index 0000000..3ce5300 Binary files /dev/null and b/icons/exit.png differ diff --git a/icons/fader_background.png b/icons/fader_background.png new file mode 100644 index 0000000..1c18375 Binary files /dev/null and b/icons/fader_background.png differ diff --git a/icons/fader_knob.png b/icons/fader_knob.png new file mode 100644 index 0000000..a1e3e26 Binary files /dev/null and b/icons/fader_knob.png differ diff --git a/icons/fader_knob_red.png b/icons/fader_knob_red.png new file mode 100644 index 0000000..10ab788 Binary files /dev/null and b/icons/fader_knob_red.png differ diff --git a/icons/fader_leds.png b/icons/fader_leds.png new file mode 100644 index 0000000..09d82eb Binary files /dev/null and b/icons/fader_leds.png differ diff --git a/icons/faders.png b/icons/faders.png new file mode 100644 index 0000000..ae4d7ce Binary files /dev/null and b/icons/faders.png differ diff --git a/icons/ffwd-end.png b/icons/ffwd-end.png new file mode 100644 index 0000000..005a268 Binary files /dev/null and b/icons/ffwd-end.png differ diff --git a/icons/ffwd.png b/icons/ffwd.png new file mode 100644 index 0000000..4efaa06 Binary files /dev/null and b/icons/ffwd.png differ diff --git a/icons/fileclose.png b/icons/fileclose.png new file mode 100644 index 0000000..edf5f76 Binary files /dev/null and b/icons/fileclose.png differ diff --git a/icons/filenew-22.png b/icons/filenew-22.png new file mode 100644 index 0000000..7be37e8 Binary files /dev/null and b/icons/filenew-22.png differ diff --git a/icons/filenew.png b/icons/filenew.png new file mode 100644 index 0000000..83f3752 Binary files /dev/null and b/icons/filenew.png differ diff --git a/icons/fileopen-22.png b/icons/fileopen-22.png new file mode 100644 index 0000000..a79982e Binary files /dev/null and b/icons/fileopen-22.png differ diff --git a/icons/fileopen.png b/icons/fileopen.png new file mode 100644 index 0000000..037c2da Binary files /dev/null and b/icons/fileopen.png differ diff --git a/icons/fileopenaudio.png b/icons/fileopenaudio.png new file mode 100644 index 0000000..45e8478 Binary files /dev/null and b/icons/fileopenaudio.png differ diff --git a/icons/fileopensession.png b/icons/fileopensession.png new file mode 100644 index 0000000..bebc235 Binary files /dev/null and b/icons/fileopensession.png differ diff --git a/icons/filesave-22.png b/icons/filesave-22.png new file mode 100644 index 0000000..3bc2a37 Binary files /dev/null and b/icons/filesave-22.png differ diff --git a/icons/filesave.png b/icons/filesave.png new file mode 100644 index 0000000..41b3f43 Binary files /dev/null and b/icons/filesave.png differ diff --git a/icons/filesaveas-22.png b/icons/filesaveas-22.png new file mode 100644 index 0000000..c8d4816 Binary files /dev/null and b/icons/filesaveas-22.png differ diff --git a/icons/filesaveas.png b/icons/filesaveas.png new file mode 100644 index 0000000..3e28d5d Binary files /dev/null and b/icons/filesaveas.png differ diff --git a/icons/help.png b/icons/help.png new file mode 100644 index 0000000..4ed65a9 Binary files /dev/null and b/icons/help.png differ diff --git a/icons/hh.gif b/icons/hh.gif new file mode 100644 index 0000000..8113a86 Binary files /dev/null and b/icons/hh.gif differ diff --git a/icons/hh.png b/icons/hh.png new file mode 100644 index 0000000..09cd3f9 Binary files /dev/null and b/icons/hh.png differ diff --git a/icons/info.png b/icons/info.png new file mode 100644 index 0000000..587d015 Binary files /dev/null and b/icons/info.png differ diff --git a/icons/instants.png b/icons/instants.png new file mode 100644 index 0000000..7417331 Binary files /dev/null and b/icons/instants.png differ diff --git a/icons/measure.png b/icons/measure.png new file mode 100644 index 0000000..4c7f4d7 Binary files /dev/null and b/icons/measure.png differ diff --git a/icons/measure1cursor.xbm b/icons/measure1cursor.xbm new file mode 100644 index 0000000..ca25e20 --- /dev/null +++ b/icons/measure1cursor.xbm @@ -0,0 +1,16 @@ +#define measure1cursor_width 32 +#define measure1cursor_height 32 +#define measure1cursor_x_hot 15 +#define measure1cursor_y_hot 14 +static unsigned char measure1cursor_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, + 0x00, 0xc0, 0x00, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0xf0, 0x00, 0x00, + 0x00, 0x98, 0x00, 0x00, 0x00, 0x8c, 0x00, 0x00, 0x00, 0x86, 0x00, 0x00, + 0x00, 0x83, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0xe0, 0x3f, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; diff --git a/icons/measure1mask.xbm b/icons/measure1mask.xbm new file mode 100644 index 0000000..85bcbf4 --- /dev/null +++ b/icons/measure1mask.xbm @@ -0,0 +1,14 @@ +#define measure1mask_width 32 +#define measure1mask_height 32 +static unsigned char measure1mask_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xc0, 0x01, 0x00, + 0x00, 0xe0, 0x01, 0x00, 0x00, 0xf0, 0x01, 0x00, 0x00, 0xf8, 0x01, 0x00, + 0x00, 0xfc, 0x01, 0x00, 0x00, 0xde, 0x01, 0x00, 0x00, 0xcf, 0x01, 0x00, + 0x80, 0xc7, 0x01, 0x00, 0xe0, 0x3f, 0x00, 0x00, 0xe0, 0x3f, 0x00, 0x00, + 0xe0, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; diff --git a/icons/measure2cursor.xbm b/icons/measure2cursor.xbm new file mode 100644 index 0000000..15a62f3 --- /dev/null +++ b/icons/measure2cursor.xbm @@ -0,0 +1,16 @@ +#define measure2cursor_width 32 +#define measure2cursor_height 32 +#define measure2cursor_x_hot 16 +#define measure2cursor_y_hot 17 +static unsigned char measure2cursor_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x07, + 0x00, 0x00, 0xc0, 0x01, 0x00, 0x00, 0xc1, 0x00, 0x00, 0x00, 0x61, 0x00, + 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, 0x0f, 0x00, + 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; diff --git a/icons/measure2mask.xbm b/icons/measure2mask.xbm new file mode 100644 index 0000000..c209a7b --- /dev/null +++ b/icons/measure2mask.xbm @@ -0,0 +1,14 @@ +#define measure2mask_width 32 +#define measure2mask_height 32 +static unsigned char measure2mask_bits[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x07, 0x00, 0x00, 0xfc, 0x07, + 0x00, 0x00, 0xfc, 0x07, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x80, 0xf3, 0x00, + 0x00, 0x80, 0x7b, 0x00, 0x00, 0x80, 0x3f, 0x00, 0x00, 0x80, 0x1f, 0x00, + 0x00, 0x80, 0x0f, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x80, 0x03, 0x00, + 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; diff --git a/icons/mono.png b/icons/mono.png new file mode 100644 index 0000000..2f0f2e0 Binary files /dev/null and b/icons/mono.png differ diff --git a/icons/move.png b/icons/move.png new file mode 100644 index 0000000..75da34d Binary files /dev/null and b/icons/move.png differ diff --git a/icons/navigate.png b/icons/navigate.png new file mode 100644 index 0000000..2fb9e7b Binary files /dev/null and b/icons/navigate.png differ diff --git a/icons/new.png b/icons/new.png new file mode 100644 index 0000000..5e76158 Binary files /dev/null and b/icons/new.png differ diff --git a/icons/notes.png b/icons/notes.png new file mode 100644 index 0000000..5a4eca3 Binary files /dev/null and b/icons/notes.png differ diff --git a/icons/pane.png b/icons/pane.png new file mode 100644 index 0000000..858342f Binary files /dev/null and b/icons/pane.png differ diff --git a/icons/playloop.png b/icons/playloop.png new file mode 100644 index 0000000..1e9bfe5 Binary files /dev/null and b/icons/playloop.png differ diff --git a/icons/playpause-orig.png b/icons/playpause-orig.png new file mode 100644 index 0000000..b50fae5 Binary files /dev/null and b/icons/playpause-orig.png differ diff --git a/icons/playpause.png b/icons/playpause.png new file mode 100644 index 0000000..606340f Binary files /dev/null and b/icons/playpause.png differ diff --git a/icons/playselection.png b/icons/playselection.png new file mode 100644 index 0000000..10fa20f Binary files /dev/null and b/icons/playselection.png differ diff --git a/icons/playselectionloop.png b/icons/playselectionloop.png new file mode 100644 index 0000000..3ec3f53 Binary files /dev/null and b/icons/playselectionloop.png differ diff --git a/icons/redo.png b/icons/redo.png new file mode 100644 index 0000000..ec2ed78 Binary files /dev/null and b/icons/redo.png differ diff --git a/icons/rewind-start.png b/icons/rewind-start.png new file mode 100644 index 0000000..32ef65b Binary files /dev/null and b/icons/rewind-start.png differ diff --git a/icons/rewind.png b/icons/rewind.png new file mode 100644 index 0000000..7091c73 Binary files /dev/null and b/icons/rewind.png differ diff --git a/icons/select.png b/icons/select.png new file mode 100644 index 0000000..0bee367 Binary files /dev/null and b/icons/select.png differ diff --git a/icons/sharpen.png b/icons/sharpen.png new file mode 100644 index 0000000..d42e937 Binary files /dev/null and b/icons/sharpen.png differ diff --git a/icons/solo.png b/icons/solo.png new file mode 100644 index 0000000..0fcafba Binary files /dev/null and b/icons/solo.png differ diff --git a/icons/speaker.png b/icons/speaker.png new file mode 100644 index 0000000..cc030d3 Binary files /dev/null and b/icons/speaker.png differ diff --git a/icons/spectrogram-22x22.png b/icons/spectrogram-22x22.png new file mode 100644 index 0000000..66f7070 Binary files /dev/null and b/icons/spectrogram-22x22.png differ diff --git a/icons/spectrogram.png b/icons/spectrogram.png new file mode 100644 index 0000000..5561fef Binary files /dev/null and b/icons/spectrogram.png differ diff --git a/icons/spectrum.png b/icons/spectrum.png new file mode 100644 index 0000000..a3c4f9e Binary files /dev/null and b/icons/spectrum.png differ diff --git a/icons/stereo.png b/icons/stereo.png new file mode 100644 index 0000000..7c81f77 Binary files /dev/null and b/icons/stereo.png differ diff --git a/icons/sv-128x128.png b/icons/sv-128x128.png new file mode 100644 index 0000000..3279afe Binary files /dev/null and b/icons/sv-128x128.png differ diff --git a/icons/sv-16x16.png b/icons/sv-16x16.png new file mode 100644 index 0000000..67c8dd0 Binary files /dev/null and b/icons/sv-16x16.png differ diff --git a/icons/sv-22x22.png b/icons/sv-22x22.png new file mode 100644 index 0000000..df2515c Binary files /dev/null and b/icons/sv-22x22.png differ diff --git a/icons/sv-24x24.png b/icons/sv-24x24.png new file mode 100644 index 0000000..0760f91 Binary files /dev/null and b/icons/sv-24x24.png differ diff --git a/icons/sv-32x32.png b/icons/sv-32x32.png new file mode 100644 index 0000000..268162f Binary files /dev/null and b/icons/sv-32x32.png differ diff --git a/icons/sv-48x48.png b/icons/sv-48x48.png new file mode 100644 index 0000000..8cad454 Binary files /dev/null and b/icons/sv-48x48.png differ diff --git a/icons/sv-64x64.png b/icons/sv-64x64.png new file mode 100644 index 0000000..ffefd6b Binary files /dev/null and b/icons/sv-64x64.png differ diff --git a/icons/sv-icon-light.svg b/icons/sv-icon-light.svg new file mode 100644 index 0000000..2238c91 --- /dev/null +++ b/icons/sv-icon-light.svg @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/icons/sv-icon.svg b/icons/sv-icon.svg new file mode 100644 index 0000000..fa2ff60 --- /dev/null +++ b/icons/sv-icon.svg @@ -0,0 +1,507 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/icons/sv-macicon.icns b/icons/sv-macicon.icns new file mode 100644 index 0000000..dedc78f Binary files /dev/null and b/icons/sv-macicon.icns differ diff --git a/icons/svicon16.png b/icons/svicon16.png new file mode 100644 index 0000000..3cb92cd Binary files /dev/null and b/icons/svicon16.png differ diff --git a/icons/svicon32.png b/icons/svicon32.png new file mode 100644 index 0000000..ff6dc8e Binary files /dev/null and b/icons/svicon32.png differ diff --git a/icons/text.png b/icons/text.png new file mode 100644 index 0000000..65ee7e1 Binary files /dev/null and b/icons/text.png differ diff --git a/icons/timeruler.png b/icons/timeruler.png new file mode 100644 index 0000000..99566ee Binary files /dev/null and b/icons/timeruler.png differ diff --git a/icons/undo.png b/icons/undo.png new file mode 100644 index 0000000..d2ed14f Binary files /dev/null and b/icons/undo.png differ diff --git a/icons/values.png b/icons/values.png new file mode 100644 index 0000000..72d628d Binary files /dev/null and b/icons/values.png differ diff --git a/icons/waveform.png b/icons/waveform.png new file mode 100644 index 0000000..de60dd1 Binary files /dev/null and b/icons/waveform.png differ diff --git a/icons/zoom-fit.png b/icons/zoom-fit.png new file mode 100644 index 0000000..001ea7a Binary files /dev/null and b/icons/zoom-fit.png differ diff --git a/icons/zoom-in.png b/icons/zoom-in.png new file mode 100644 index 0000000..d2b139c Binary files /dev/null and b/icons/zoom-in.png differ diff --git a/icons/zoom-out.png b/icons/zoom-out.png new file mode 100644 index 0000000..2ae727f Binary files /dev/null and b/icons/zoom-out.png differ diff --git a/icons/zoom.png b/icons/zoom.png new file mode 100644 index 0000000..bee286a Binary files /dev/null and b/icons/zoom.png differ diff --git a/main/MainWindow.cpp b/main/MainWindow.cpp new file mode 100644 index 0000000..b7dcda6 --- /dev/null +++ b/main/MainWindow.cpp @@ -0,0 +1,5437 @@ +/* -*- 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 and QMUL. + + 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/Document.h" +#include "PreferencesDialog.h" + +#include "view/Pane.h" +#include "view/PaneStack.h" +#include "data/model/WaveFileModel.h" +#include "data/model/SparseOneDimensionalModel.h" +#include "view/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 "layer/SliceLayer.h" +#include "layer/SliceableLayer.h" +#include "widgets/Fader.h" +#include "view/Overview.h" +#include "widgets/PropertyBox.h" +#include "widgets/PropertyStack.h" +#include "widgets/AudioDial.h" +#include "widgets/IconLoader.h" +#include "widgets/LayerTree.h" +#include "widgets/ListInputDialog.h" +#include "widgets/SubdividingMenu.h" +#include "widgets/NotifyingPushButton.h" +#include "widgets/KeyReference.h" +#include "audioio/AudioCallbackPlaySource.h" +#include "audioio/AudioCallbackPlayTarget.h" +#include "audioio/AudioTargetFactory.h" +#include "audioio/PlaySpeedRangeMapper.h" +#include "data/fileio/DataFileReaderFactory.h" +#include "data/fileio/PlaylistFileReader.h" +#include "data/fileio/WavFileWriter.h" +#include "data/fileio/CSVFileWriter.h" +#include "data/fileio/BZipFileDevice.h" +#include "data/fileio/RemoteFile.h" +#include "data/fft/FFTDataServer.h" +#include "base/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" +#include "base/UnitDatabase.h" +#include "base/ColourDatabase.h" +#include "osc/OSCQueue.h" + +//!!! +#include "data/model/AggregateWaveModel.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 +#include +#include +#include +#include + +#include +#include +#include + +using std::cerr; +using std::endl; + +using std::vector; +using std::map; +using std::set; + + +MainWindow::MainWindow(bool withAudioOutput, bool withOSCSupport) : + m_document(0), + m_paneStack(0), + m_viewManager(0), + m_overview(0), + m_timeRulerLayer(0), + m_audioOutput(withAudioOutput), + m_playSource(0), + m_playTarget(0), + m_oscQueue(withOSCSupport ? new OSCQueue() : 0), + m_recentFiles("RecentFiles", 20), + m_recentTransforms("RecentTransforms", 20), + m_mainMenusCreated(false), + m_paneMenu(0), + m_layerMenu(0), + m_transformsMenu(0), + m_playbackMenu(0), + m_existingLayersMenu(0), + m_sliceMenu(0), + m_recentFilesMenu(0), + m_recentTransformsMenu(0), + m_rightButtonMenu(0), + m_rightButtonLayerMenu(0), + m_rightButtonTransformsMenu(0), + m_rightButtonPlaybackMenu(0), + m_deleteSelectedAction(0), + m_ffwdAction(0), + m_rwdAction(0), + m_documentModified(false), + m_openingAudioFile(false), + m_abandoning(false), + m_preferencesDialog(0), + m_layerTreeView(0), + m_keyReference(new KeyReference()) +{ + setWindowTitle(tr("Vect")); + + UnitDatabase *udb = UnitDatabase::getInstance(); + udb->registerUnit("Hz"); + udb->registerUnit("dB"); + udb->registerUnit("s"); + + ColourDatabase *cdb = ColourDatabase::getInstance(); + cdb->addColour(Qt::black, tr("Black")); + cdb->addColour(Qt::darkRed, tr("Red")); + cdb->addColour(Qt::darkBlue, tr("Blue")); + cdb->addColour(Qt::darkGreen, tr("Green")); + cdb->addColour(QColor(200, 50, 255), tr("Purple")); + cdb->addColour(QColor(255, 150, 50), tr("Orange")); + cdb->setUseDarkBackground(cdb->addColour(Qt::white, tr("White")), true); + cdb->setUseDarkBackground(cdb->addColour(Qt::red, tr("Bright Red")), true); + cdb->setUseDarkBackground(cdb->addColour(QColor(30, 150, 255), tr("Bright Blue")), true); + cdb->setUseDarkBackground(cdb->addColour(Qt::green, tr("Bright Green")), true); + cdb->setUseDarkBackground(cdb->addColour(QColor(225, 74, 255), tr("Bright Purple")), true); + cdb->setUseDarkBackground(cdb->addColour(QColor(255, 188, 80), tr("Bright Orange")), true); + + 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())); + connect(m_viewManager, SIGNAL(inProgressSelectionChanged()), + this, SLOT(inProgressSelectionChanged())); + m_viewManager->setPlaySoloMode(true); + + Preferences::BackgroundMode mode = + Preferences::getInstance()->getBackgroundMode(); + m_initialDarkBackground = m_viewManager->getGlobalDarkBackground(); + if (mode != Preferences::BackgroundFromTheme) { + m_viewManager->setGlobalDarkBackground + (mode == Preferences::DarkBackground); + } + + m_descriptionLabel = new QLabel; + + QScrollArea *scroll = new QScrollArea(frame); + scroll->setWidgetResizable(true); + scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + scroll->setFrameShape(QFrame::NoFrame); + + m_paneStack = new PaneStack(scroll, m_viewManager); + m_paneStack->setLayoutStyle(PaneStack::NoPropertyStacks); + + 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))); + connect(m_paneStack, SIGNAL(propertyStacksResized()), + this, SLOT(propertyStacksResized())); + connect(m_paneStack, SIGNAL(contextHelpChanged(const QString &)), + this, SLOT(contextHelpChanged(const QString &))); + + scroll->setWidget(m_paneStack); + + m_overview = new Overview(frame); + m_overview->setViewManager(m_viewManager); + m_overview->setFixedHeight(40); +#ifndef _WIN32 + // For some reason, the contents of the overview never appear if we + // make this setting on Windows. I have no inclination at the moment + // to track down the reason why. + m_overview->setFrameStyle(QFrame::StyledPanel | QFrame::Sunken); +#endif + connect(m_overview, SIGNAL(contextHelpChanged(const QString &)), + this, SLOT(contextHelpChanged(const QString &))); + m_overview->hide(); + + m_panLayer = new WaveformLayer; + m_panLayer->setChannelMode(WaveformLayer::MergeChannels); + m_panLayer->setAggressiveCacheing(true); + m_overview->addLayer(m_panLayer); + + if (m_viewManager->getGlobalDarkBackground()) { + m_panLayer->setBaseColour + (ColourDatabase::getInstance()->getColourIndex(tr("Bright Green"))); + } else { + m_panLayer->setBaseColour + (ColourDatabase::getInstance()->getColourIndex(tr("Green"))); + } + + m_playSource = new AudioCallbackPlaySource(m_viewManager); + + connect(m_playSource, SIGNAL(sampleRateMismatch(size_t, size_t, bool)), + this, SLOT(sampleRateMismatch(size_t, size_t, bool))); + connect(m_playSource, SIGNAL(audioOverloadPluginDisabled()), + this, SLOT(audioOverloadPluginDisabled())); + + m_fader = new Fader(frame, false); + connect(m_fader, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget())); + connect(m_fader, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget())); + + m_playSpeed = new AudioDial(frame); + m_playSpeed->setMinimum(0); + m_playSpeed->setMaximum(200); + m_playSpeed->setValue(100); + m_playSpeed->setFixedWidth(24); + m_playSpeed->setFixedHeight(24); + m_playSpeed->setNotchesVisible(true); + m_playSpeed->setPageStep(10); + m_playSpeed->setObjectName(tr("Playback Speedup")); + m_playSpeed->setDefaultValue(100); + m_playSpeed->setRangeMapper(new PlaySpeedRangeMapper(0, 200)); + m_playSpeed->setShowToolTip(true); + connect(m_playSpeed, SIGNAL(valueChanged(int)), + this, SLOT(playSpeedChanged(int))); + connect(m_playSpeed, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget())); + connect(m_playSpeed, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget())); + + IconLoader il; + + m_playSharpen = new NotifyingPushButton(frame); + m_playSharpen->setToolTip(tr("Sharpen percussive transients")); + m_playSharpen->setFixedSize(20, 20); + m_playSharpen->setEnabled(false); + m_playSharpen->setCheckable(true); + m_playSharpen->setChecked(false); + m_playSharpen->setIcon(il.load("sharpen")); + connect(m_playSharpen, SIGNAL(clicked()), this, SLOT(playSharpenToggled())); + connect(m_playSharpen, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget())); + connect(m_playSharpen, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget())); + + m_playMono = new NotifyingPushButton(frame); + m_playMono->setToolTip(tr("Run time stretcher in mono only")); + m_playMono->setFixedSize(20, 20); + m_playMono->setEnabled(false); + m_playMono->setCheckable(true); + m_playMono->setChecked(false); + m_playMono->setIcon(il.load("mono")); + connect(m_playMono, SIGNAL(clicked()), this, SLOT(playMonoToggled())); + connect(m_playMono, SIGNAL(mouseEntered()), this, SLOT(mouseEnteredWidget())); + connect(m_playMono, SIGNAL(mouseLeft()), this, SLOT(mouseLeftWidget())); + + QSettings settings; + settings.beginGroup("MainWindow"); + m_playSharpen->setChecked(settings.value("playsharpen", true).toBool()); + m_playMono->setChecked(settings.value("playmono", false).toBool()); + settings.endGroup(); + + layout->setSpacing(4); + layout->addWidget(scroll, 0, 0, 1, 5); + layout->addWidget(m_overview, 1, 0); + layout->addWidget(m_fader, 1, 1); + layout->addWidget(m_playSpeed, 1, 2); + layout->addWidget(m_playSharpen, 1, 3); + layout->addWidget(m_playMono, 1, 4); + + m_paneStack->setPropertyStackMinWidth + (m_fader->width() + m_playSpeed->width() + m_playSharpen->width() + + m_playMono->width() + layout->spacing() * 4); + + layout->setColumnStretch(0, 10); + + frame->setLayout(layout); + + connect(m_viewManager, SIGNAL(outputLevelsChanged(float, float)), + this, SLOT(outputLevelsChanged(float, float))); + + connect(m_viewManager, SIGNAL(playbackFrameChanged(unsigned long)), + this, SLOT(playbackFrameChanged(unsigned long))); + + connect(m_viewManager, SIGNAL(globalCentreFrameChanged(unsigned long)), + this, SLOT(globalCentreFrameChanged(unsigned long))); + + connect(m_viewManager, SIGNAL(viewCentreFrameChanged(View *, unsigned long)), + this, SLOT(viewCentreFrameChanged(View *, unsigned long))); + + connect(m_viewManager, SIGNAL(viewZoomLevelChanged(View *, unsigned long, bool)), + this, SLOT(viewZoomLevelChanged(View *, unsigned long, bool))); + + connect(Preferences::getInstance(), + SIGNAL(propertyChanged(PropertyContainer::PropertyName)), + this, + SLOT(preferenceChanged(PropertyContainer::PropertyName))); + +// preferenceChanged("Property Box Layout"); + + if (m_oscQueue && m_oscQueue->isOK()) { + connect(m_oscQueue, SIGNAL(messagesAvailable()), this, SLOT(pollOSC())); + QTimer *oscTimer = new QTimer(this); + connect(oscTimer, SIGNAL(timeout()), this, SLOT(pollOSC())); + oscTimer->start(1000); + } + + setupMenus(); + setupToolbars(); + setupHelpMenu(); + + statusBar(); + + newSession(); +} + +MainWindow::~MainWindow() +{ +// std::cerr << "MainWindow::~MainWindow()" << std::endl; + + if (!m_abandoning) { + closeSession(); + } + delete m_playTarget; + delete m_playSource; + delete m_viewManager; + delete m_oscQueue; + delete m_keyReference; + delete m_preferencesDialog; + delete m_layerTreeView; + Profiles::getInstance()->dump(); +} + +QString +MainWindow::getOpenFileName(FileFinder::FileType type) +{ + FileFinder *ff = FileFinder::getInstance(); + switch (type) { + case FileFinder::SessionFile: + return ff->getOpenFileName(type, m_sessionFile); + case FileFinder::AudioFile: + return ff->getOpenFileName(type, m_audioFile); + case FileFinder::LayerFile: + return ff->getOpenFileName(type, m_sessionFile); + case FileFinder::SessionOrAudioFile: + return ff->getOpenFileName(type, m_sessionFile); + case FileFinder::ImageFile: + return ff->getOpenFileName(type, m_sessionFile); + case FileFinder::AnyFile: + if (getMainModel() != 0 && + m_paneStack != 0 && + m_paneStack->getCurrentPane() != 0) { // can import a layer + return ff->getOpenFileName(FileFinder::AnyFile, m_sessionFile); + } else { + return ff->getOpenFileName(FileFinder::SessionOrAudioFile, + m_sessionFile); + } + } + return ""; +} + +QString +MainWindow::getSaveFileName(FileFinder::FileType type) +{ + FileFinder *ff = FileFinder::getInstance(); + switch (type) { + case FileFinder::SessionFile: + return ff->getSaveFileName(type, m_sessionFile); + case FileFinder::AudioFile: + return ff->getSaveFileName(type, m_audioFile); + case FileFinder::LayerFile: + return ff->getSaveFileName(type, m_sessionFile); + case FileFinder::SessionOrAudioFile: + return ff->getSaveFileName(type, m_sessionFile); + case FileFinder::ImageFile: + return ff->getSaveFileName(type, m_sessionFile); + case FileFinder::AnyFile: + return ff->getSaveFileName(type, m_sessionFile); + } + return ""; +} + +void +MainWindow::registerLastOpenedFilePath(FileFinder::FileType type, QString path) +{ + FileFinder *ff = FileFinder::getInstance(); + ff->registerLastOpenedFilePath(type, path); +} + +void +MainWindow::setupMenus() +{ + if (!m_mainMenusCreated) { + m_rightButtonMenu = new QMenu(); + + // No -- we don't want tear-off enabled on the right-button + // menu. If it is enabled, then simply right-clicking and + // releasing will pop up the menu, activate the tear-off, and + // leave the torn-off menu window in front of the main window. + // That isn't desirable. I'm not sure it ever would be, in a + // context menu -- perhaps technically a Qt bug? +// m_rightButtonMenu->setTearOffEnabled(true); + } + + if (m_rightButtonLayerMenu) { + m_rightButtonLayerMenu->clear(); + } else { + m_rightButtonLayerMenu = m_rightButtonMenu->addMenu(tr("&Layer")); + m_rightButtonLayerMenu->setTearOffEnabled(true); + m_rightButtonMenu->addSeparator(); + } + + if (m_rightButtonTransformsMenu) { + m_rightButtonTransformsMenu->clear(); + } else { + m_rightButtonTransformsMenu = m_rightButtonMenu->addMenu(tr("&Transform")); + m_rightButtonTransformsMenu->setTearOffEnabled(true); + m_rightButtonMenu->addSeparator(); + } + + if (!m_mainMenusCreated) { + CommandHistory::getInstance()->registerMenu(m_rightButtonMenu); + m_rightButtonMenu->addSeparator(); + } + + setupFileMenu(); + setupEditMenu(); + setupViewMenu(); + setupPaneAndLayerMenus(); + setupTransformsMenu(); + + m_mainMenusCreated = true; +} + +void +MainWindow::setupFileMenu() +{ + if (m_mainMenusCreated) return; + + QMenu *menu = menuBar()->addMenu(tr("&File")); + menu->setTearOffEnabled(true); + QToolBar *toolbar = addToolBar(tr("File Toolbar")); + + m_keyReference->setCategory(tr("File and Session Management")); + + IconLoader il; + + QIcon icon = il.load("filenew"); + icon.addPixmap(il.loadPixmap("filenew-22")); + QAction *action = new QAction(icon, tr("&New Session"), this); + action->setShortcut(tr("Ctrl+N")); + action->setStatusTip(tr("Abandon the current Sonic Visualiser session and start a new one")); + connect(action, SIGNAL(triggered()), this, SLOT(newSession())); + m_keyReference->registerShortcut(action); + menu->addAction(action); + toolbar->addAction(action); +/* + icon = il.load("fileopensession"); + 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())); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + icon = il.load("fileopen"); + icon.addPixmap(il.loadPixmap("fileopen-22")); + + 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 = il.load("filesave"); + icon.addPixmap(il.loadPixmap("filesave-22")); + 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))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + toolbar->addAction(action); + + icon = il.load("filesaveas"); + icon.addPixmap(il.loadPixmap("filesaveas-22")); + 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(); +*/ + icon = il.load("fileopenaudio"); + action = new QAction(icon, tr("&Open Audio File..."), this); + action->setShortcut(tr("Ctrl+O")); + action->setStatusTip(tr("Add an audio file")); + connect(action, SIGNAL(triggered()), this, SLOT(importAudio())); + m_keyReference->registerShortcut(action); + menu->addAction(action); + toolbar->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))); + m_keyReference->registerShortcut(action); + 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))); + m_keyReference->registerShortcut(action); + 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(); + + action = new QAction(tr("Export Image File..."), this); + action->setStatusTip(tr("Export a single pane to an image file")); + connect(action, SIGNAL(triggered()), this, SLOT(exportImage())); + connect(this, SIGNAL(canExportImage(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + menu->addSeparator(); +*/ + action = new QAction(tr("Open Lo&cation..."), this); + action->setShortcut(tr("Ctrl+Shift+O")); + action->setStatusTip(tr("Open or import a file from a remote URL")); + connect(action, SIGNAL(triggered()), this, SLOT(openLocation())); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + menu->addSeparator(); + + m_recentFilesMenu = menu->addMenu(tr("&Recent Files")); + m_recentFilesMenu->setTearOffEnabled(true); + setupRecentFilesMenu(); + connect(&m_recentFiles, SIGNAL(recentChanged()), + 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(il.load("exit"), + tr("&Quit"), this); + action->setShortcut(tr("Ctrl+Q")); + action->setStatusTip(tr("Exit Vect")); + connect(action, SIGNAL(triggered()), this, SLOT(close())); + m_keyReference->registerShortcut(action); + menu->addAction(action); +} + +void +MainWindow::setupEditMenu() +{ + return; //!!! + + if (m_mainMenusCreated) return; + + QMenu *menu = menuBar()->addMenu(tr("&Edit")); + menu->setTearOffEnabled(true); + CommandHistory::getInstance()->registerMenu(menu); + + m_keyReference->setCategory(tr("Editing")); + + menu->addSeparator(); + + IconLoader il; + + QAction *action = new QAction(il.load("editcut"), + tr("Cu&t"), this); + action->setShortcut(tr("Ctrl+X")); + action->setStatusTip(tr("Cut the selection from the current layer to the clipboard")); + connect(action, SIGNAL(triggered()), this, SLOT(cut())); + connect(this, SIGNAL(canEditSelection(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + action = new QAction(il.load("editcopy"), + tr("&Copy"), this); + action->setShortcut(tr("Ctrl+C")); + action->setStatusTip(tr("Copy the selection from the current layer to the clipboard")); + connect(action, SIGNAL(triggered()), this, SLOT(copy())); + connect(this, SIGNAL(canEditSelection(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + action = new QAction(il.load("editpaste"), + tr("&Paste"), this); + action->setShortcut(tr("Ctrl+V")); + action->setStatusTip(tr("Paste from the clipboard to the current layer")); + connect(action, SIGNAL(triggered()), this, SLOT(paste())); + connect(this, SIGNAL(canPaste(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + m_deleteSelectedAction = new QAction(tr("&Delete Selected Items"), this); + m_deleteSelectedAction->setShortcut(tr("Del")); + m_deleteSelectedAction->setStatusTip(tr("Delete items in current selection from the current layer")); + connect(m_deleteSelectedAction, SIGNAL(triggered()), this, SLOT(deleteSelected())); + connect(this, SIGNAL(canDeleteSelection(bool)), m_deleteSelectedAction, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(m_deleteSelectedAction); + menu->addAction(m_deleteSelectedAction); + m_rightButtonMenu->addAction(m_deleteSelectedAction); + + menu->addSeparator(); + m_rightButtonMenu->addSeparator(); + + m_keyReference->setCategory(tr("Selection")); + + action = new QAction(tr("Select &All"), this); + action->setShortcut(tr("Ctrl+A")); + action->setStatusTip(tr("Select the whole duration of the current session")); + connect(action, SIGNAL(triggered()), this, SLOT(selectAll())); + connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + action = new QAction(tr("Select &Visible Range"), this); + action->setShortcut(tr("Ctrl+Shift+A")); + action->setStatusTip(tr("Select the time range corresponding to the current window width")); + connect(action, SIGNAL(triggered()), this, SLOT(selectVisible())); + connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("Select to &Start"), this); + action->setShortcut(tr("Shift+Left")); + action->setStatusTip(tr("Select from the start of the session to the current playback position")); + connect(action, SIGNAL(triggered()), this, SLOT(selectToStart())); + connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("Select to &End"), this); + action->setShortcut(tr("Shift+Right")); + action->setStatusTip(tr("Select from the current playback position to the end of the session")); + connect(action, SIGNAL(triggered()), this, SLOT(selectToEnd())); + connect(this, SIGNAL(canSelect(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("C&lear Selection"), this); + action->setShortcut(tr("Esc")); + action->setStatusTip(tr("Clear the selection")); + connect(action, SIGNAL(triggered()), this, SLOT(clearSelection())); + connect(this, SIGNAL(canClearSelection(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + m_rightButtonMenu->addAction(action); + + menu->addSeparator(); + + m_keyReference->setCategory(tr("Tapping Time Instants")); + + action = new QAction(tr("&Insert Instant at Playback Position"), this); + action->setShortcut(tr("Enter")); + action->setStatusTip(tr("Insert a new time instant at the current playback position, in a new layer if necessary")); + connect(action, SIGNAL(triggered()), this, SLOT(insertInstant())); + connect(this, SIGNAL(canInsertInstant(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + // Laptop shortcut (no keypad Enter key) + QString shortcut(tr(";")); + connect(new QShortcut(shortcut, this), SIGNAL(activated()), + this, SLOT(insertInstant())); + m_keyReference->registerAlternativeShortcut(action, shortcut); + + action = new QAction(tr("Insert Instants at Selection &Boundaries"), this); + action->setShortcut(tr("Shift+Enter")); + action->setStatusTip(tr("Insert new time instants at the start and end of the current selected regions, in a new layer if necessary")); + connect(action, SIGNAL(triggered()), this, SLOT(insertInstantsAtBoundaries())); + connect(this, SIGNAL(canInsertInstantsAtBoundaries(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); +} + +void +MainWindow::setupViewMenu() +{ + if (m_mainMenusCreated) return; + + IconLoader il; + + QAction *action = 0; + + m_keyReference->setCategory(tr("Panning and Navigation")); + + QMenu *menu = menuBar()->addMenu(tr("&View")); + menu->setTearOffEnabled(true); + 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))); + m_keyReference->registerShortcut(action); + 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))); + m_keyReference->registerShortcut(action); + 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))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("J&ump 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))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + menu->addSeparator(); + + m_keyReference->setCategory(tr("Zoom")); + + action = new QAction(il.load("zoom-in"), + 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))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(il.load("zoom-out"), + 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))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("Restore &Default Zoom"), this); + action->setStatusTip(tr("Restore the zoom level to the default")); + connect(action, SIGNAL(triggered()), this, SLOT(zoomDefault())); + connect(this, SIGNAL(canZoom(bool)), action, SLOT(setEnabled(bool))); + menu->addAction(action); + + action = new QAction(il.load("zoom-fit"), + tr("Zoom to &Fit"), this); + action->setShortcut(tr("F")); + 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))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + menu->addSeparator(); + + m_keyReference->setCategory(tr("Display Features")); + + QActionGroup *overlayGroup = new QActionGroup(this); + + action = new QAction(tr("Show &No Overlays"), this); + action->setShortcut(tr("0")); + action->setStatusTip(tr("Hide centre indicator, frame times, layer names and scale")); + connect(action, SIGNAL(triggered()), this, SLOT(showNoOverlays())); + action->setCheckable(true); + action->setChecked(false); + overlayGroup->addAction(action); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("Show &Minimal Overlays"), this); + action->setShortcut(tr("9")); + action->setStatusTip(tr("Show centre indicator only")); + connect(action, SIGNAL(triggered()), this, SLOT(showMinimalOverlays())); + action->setCheckable(true); + action->setChecked(false); + overlayGroup->addAction(action); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("Show &Standard Overlays"), this); + action->setShortcut(tr("8")); + action->setStatusTip(tr("Show centre indicator, frame times and scale")); + connect(action, SIGNAL(triggered()), this, SLOT(showStandardOverlays())); + action->setCheckable(true); + action->setChecked(true); + overlayGroup->addAction(action); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("Show &All Overlays"), this); + action->setShortcut(tr("7")); + action->setStatusTip(tr("Show all texts and scale")); + connect(action, SIGNAL(triggered()), this, SLOT(showAllOverlays())); + action->setCheckable(true); + action->setChecked(false); + overlayGroup->addAction(action); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + menu->addSeparator(); + + action = new QAction(tr("Show &Zoom Wheels"), this); + action->setShortcut(tr("Z")); + action->setStatusTip(tr("Show thumbwheels for zooming horizontally and vertically")); + connect(action, SIGNAL(triggered()), this, SLOT(toggleZoomWheels())); + action->setCheckable(true); + action->setChecked(m_viewManager->getZoomWheelsEnabled()); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("Show Property Bo&xes"), this); + action->setShortcut(tr("X")); + action->setStatusTip(tr("Show the layer property boxes at the side of the main window")); + connect(action, SIGNAL(triggered()), this, SLOT(togglePropertyBoxes())); + action->setCheckable(true); + action->setChecked(false); //!!! + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("Show Status &Bar"), this); + action->setStatusTip(tr("Show context help information in the status bar at the bottom of the window")); + connect(action, SIGNAL(triggered()), this, SLOT(toggleStatusBar())); + action->setCheckable(true); + action->setChecked(true); + menu->addAction(action); + + QSettings settings; + settings.beginGroup("MainWindow"); + bool sb = settings.value("showstatusbar", true).toBool(); + if (!sb) { + action->setChecked(false); + statusBar()->hide(); + } + settings.endGroup(); + + menu->addSeparator(); + + action = new QAction(tr("Show La&yer Hierarchy"), this); + action->setShortcut(tr("H")); + action->setStatusTip(tr("Open a window displaying the hierarchy of panes and layers in this session")); + connect(action, SIGNAL(triggered()), this, SLOT(showLayerTree())); + m_keyReference->registerShortcut(action); + menu->addAction(action); +} + +void +MainWindow::setupPaneAndLayerMenus() +{ + return; //!!! + + if (m_paneMenu) { + m_paneActions.clear(); + m_paneMenu->clear(); + } else { + m_paneMenu = menuBar()->addMenu(tr("&Pane")); + m_paneMenu->setTearOffEnabled(true); + } + + if (m_layerMenu) { + m_layerActions.clear(); + m_layerMenu->clear(); + } else { + m_layerMenu = menuBar()->addMenu(tr("&Layer")); + m_layerMenu->setTearOffEnabled(true); + } + + QMenu *menu = m_paneMenu; + + IconLoader il; + + m_keyReference->setCategory(tr("Managing Panes and Layers")); + + QAction *action = new QAction(il.load("pane"), tr("Add &New Pane"), this); + action->setShortcut(tr("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); + m_keyReference->registerShortcut(action); + 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 = il.load(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("T")); + m_keyReference->registerShortcut(action); + } + + 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(); + + LayerFactory::LayerType backgroundTypes[] = { + LayerFactory::Waveform, + LayerFactory::Spectrogram, + LayerFactory::MelodicRangeSpectrogram, + LayerFactory::PeakFrequencySpectrogram, + LayerFactory::Spectrum + }; + + std::vector models; + if (m_document) models = m_document->getTransformInputModels(); //!!! not well named for this! + bool plural = (models.size() > 1); + if (models.empty()) { + models.push_back(getMainModel()); // probably 0 + } + + 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; + + QIcon icon; + QString mainText, shortcutText, tipText, channelText; + LayerFactory::LayerType type = backgroundTypes[i]; + bool mono = true; + + switch (type) { + + case LayerFactory::Waveform: + icon = il.load("waveform"); + mainText = tr("Add &Waveform"); + if (menuType == 0) { + shortcutText = tr("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: + icon = il.load("spectrogram"); + mainText = tr("Add Spectro&gram"); + if (menuType == 0) { + shortcutText = tr("G"); + tipText = tr("Add a new pane showing a spectrogram"); + } else { + tipText = tr("Add a new layer showing a spectrogram"); + } + break; + + case LayerFactory::MelodicRangeSpectrogram: + icon = il.load("spectrogram"); + mainText = tr("Add &Melodic Range Spectrogram"); + if (menuType == 0) { + shortcutText = tr("M"); + tipText = tr("Add a new pane showing a spectrogram set up for an overview of note pitches"); + } else { + tipText = tr("Add a new layer showing a spectrogram set up for an overview of note pitches"); + } + break; + + case LayerFactory::PeakFrequencySpectrogram: + icon = il.load("spectrogram"); + mainText = tr("Add Pea&k Frequency Spectrogram"); + if (menuType == 0) { + shortcutText = tr("K"); + 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; + + case LayerFactory::Spectrum: + icon = il.load("spectrum"); + mainText = tr("Add Spectr&um"); + if (menuType == 0) { + shortcutText = tr("U"); + tipText = tr("Add a new pane showing a frequency spectrum"); + } else { + tipText = tr("Add a new layer showing a frequency spectrum"); + } + break; + + default: break; + } + + std::vector candidateModels; + if (menuType == 0) { + candidateModels = models; + } else { + candidateModels.push_back(0); + } + + for (std::vector::iterator mi = + candidateModels.begin(); + mi != candidateModels.end(); ++mi) { + + Model *model = *mi; + + int channels = 0; + if (model) { + DenseTimeValueModel *dtvm = + dynamic_cast(model); + if (dtvm) channels = dtvm->getChannelCount(); + } + if (channels < 1 && getMainModel()) { + channels = getMainModel()->getChannelCount(); + } + if (channels < 1) channels = 1; + + 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; + } + + if (isOnly && (!plural || menuType == 1)) { + + if (menuType == 1 && type != LayerFactory::Waveform) { + action = new QAction(mainText, this); + } else { + 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; + } + if (shortcutText != "") { + m_keyReference->registerShortcut(action); + } + menu->addAction(action); + + } else { + + if (!submenu) { + submenu = menu->addMenu(mainText); + submenu->setTearOffEnabled(true); + } else if (isDefault) { + submenu->addSeparator(); + } + + 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 (model) { + actionText = tr("%1: %2") + .arg(model->objectName()) + .arg(actionText); + } + + if (isDefault) { + action = new QAction(icon, actionText, this); + if (!model || model == getMainModel()) { + action->setShortcut(shortcutText); + } + } else { + action = new QAction(actionText, this); + } + + 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, model, 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(il.load("editdelete"), tr("&Delete Pane"), this); + action->setShortcut(tr("Ctrl+Shift+D")); + action->setStatusTip(tr("Delete the currently active pane")); + connect(action, SIGNAL(triggered()), this, SLOT(deleteCurrentPane())); + connect(this, SIGNAL(canDeleteCurrentPane(bool)), action, SLOT(setEnabled(bool))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + menu = m_layerMenu; + + action = new QAction(il.load("timeruler"), 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_existingLayersMenu->setTearOffEnabled(true); + m_rightButtonLayerMenu->addMenu(m_existingLayersMenu); + + m_sliceMenu = menu->addMenu(tr("Add S&lice of Layer")); + m_sliceMenu->setTearOffEnabled(true); + m_rightButtonLayerMenu->addMenu(m_sliceMenu); + + setupExistingLayersMenus(); + + m_rightButtonLayerMenu->addSeparator(); + menu->addSeparator(); + + QAction *raction = new QAction(tr("&Rename Layer..."), this); + raction->setShortcut(tr("R")); + raction->setStatusTip(tr("Rename the currently active layer")); + connect(raction, SIGNAL(triggered()), this, SLOT(renameCurrentLayer())); + connect(this, SIGNAL(canRenameLayer(bool)), raction, SLOT(setEnabled(bool))); + menu->addAction(raction); + m_rightButtonLayerMenu->addAction(raction); + + action = new QAction(il.load("editdelete"), tr("&Delete Layer"), this); + action->setShortcut(tr("Ctrl+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))); + m_keyReference->registerShortcut(action); + menu->addAction(action); + m_rightButtonLayerMenu->addAction(action); + + m_keyReference->registerShortcut(raction); // rename after delete, so delete layer goes next to delete pane +} + +void +MainWindow::setupTransformsMenu() +{ + return; //!!! + + if (m_transformsMenu) { + m_transformActions.clear(); + m_transformActionsReverse.clear(); + m_transformsMenu->clear(); + } else { + m_transformsMenu = menuBar()->addMenu(tr("&Transform")); + m_transformsMenu->setTearOffEnabled(true); + } + + TransformFactory::TransformList transforms = + TransformFactory::getInstance()->getAllTransforms(); + + vector types = + TransformFactory::getInstance()->getAllTransformTypes(); + + map > categoryMenus; + map > makerMenus; + + map byPluginNameMenus; + map > pluginNameMenus; + + set pendingMenus; + + m_recentTransformsMenu = m_transformsMenu->addMenu(tr("&Recent Transforms")); + m_recentTransformsMenu->setTearOffEnabled(true); + m_rightButtonTransformsMenu->addMenu(m_recentTransformsMenu); + connect(&m_recentTransforms, SIGNAL(recentChanged()), + this, SLOT(setupRecentTransformsMenu())); + + m_transformsMenu->addSeparator(); + m_rightButtonTransformsMenu->addSeparator(); + + for (vector::iterator i = types.begin(); i != types.end(); ++i) { + + if (i != types.begin()) { + m_transformsMenu->addSeparator(); + m_rightButtonTransformsMenu->addSeparator(); + } + + QString byCategoryLabel = tr("%1 by Category").arg(*i); + SubdividingMenu *byCategoryMenu = new SubdividingMenu(byCategoryLabel, + 20, 40); + byCategoryMenu->setTearOffEnabled(true); + m_transformsMenu->addMenu(byCategoryMenu); + m_rightButtonTransformsMenu->addMenu(byCategoryMenu); + pendingMenus.insert(byCategoryMenu); + + vector categories = + TransformFactory::getInstance()->getTransformCategories(*i); + + for (vector::iterator j = categories.begin(); + j != categories.end(); ++j) { + + QString category = *j; + if (category == "") category = tr("Unclassified"); + + if (categories.size() < 2) { + categoryMenus[*i][category] = byCategoryMenu; + continue; + } + + QStringList components = category.split(" > "); + QString key; + + for (QStringList::iterator k = components.begin(); + k != components.end(); ++k) { + + QString parentKey = key; + if (key != "") key += " > "; + key += *k; + + if (categoryMenus[*i].find(key) == categoryMenus[*i].end()) { + SubdividingMenu *m = new SubdividingMenu(*k, 20, 40); + m->setTearOffEnabled(true); + pendingMenus.insert(m); + categoryMenus[*i][key] = m; + if (parentKey == "") { + byCategoryMenu->addMenu(m); + } else { + categoryMenus[*i][parentKey]->addMenu(m); + } + } + } + } + + QString byPluginNameLabel = tr("%1 by Plugin Name").arg(*i); + byPluginNameMenus[*i] = new SubdividingMenu(byPluginNameLabel); + byPluginNameMenus[*i]->setTearOffEnabled(true); + m_transformsMenu->addMenu(byPluginNameMenus[*i]); + m_rightButtonTransformsMenu->addMenu(byPluginNameMenus[*i]); + pendingMenus.insert(byPluginNameMenus[*i]); + + QString byMakerLabel = tr("%1 by Maker").arg(*i); + SubdividingMenu *byMakerMenu = new SubdividingMenu(byMakerLabel, 20, 40); + byMakerMenu->setTearOffEnabled(true); + m_transformsMenu->addMenu(byMakerMenu); + m_rightButtonTransformsMenu->addMenu(byMakerMenu); + pendingMenus.insert(byMakerMenu); + + vector makers = + TransformFactory::getInstance()->getTransformMakers(*i); + + for (vector::iterator j = makers.begin(); + j != makers.end(); ++j) { + + QString maker = *j; + if (maker == "") maker = tr("Unknown"); + maker.replace(QRegExp(tr(" [\\(<].*$")), ""); + + makerMenus[*i][maker] = new SubdividingMenu(maker, 30, 40); + makerMenus[*i][maker]->setTearOffEnabled(true); + byMakerMenu->addMenu(makerMenus[*i][maker]); + pendingMenus.insert(makerMenus[*i][maker]); + } + } + + for (unsigned int i = 0; i < transforms.size(); ++i) { + + QString name = transforms[i].name; + if (name == "") name = transforms[i].identifier; + +// std::cerr << "Plugin Name: " << name.toStdString() << std::endl; + + QString type = transforms[i].type; + + QString category = transforms[i].category; + if (category == "") category = tr("Unclassified"); + + QString maker = transforms[i].maker; + if (maker == "") maker = tr("Unknown"); + maker.replace(QRegExp(tr(" [\\(<].*$")), ""); + + QString pluginName = name.section(": ", 0, 0); + QString output = name.section(": ", 1); + + QAction *action = new QAction(tr("%1...").arg(name), this); + connect(action, SIGNAL(triggered()), this, SLOT(addLayer())); + m_transformActions[action] = transforms[i].identifier; + m_transformActionsReverse[transforms[i].identifier] = action; + connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool))); + + action->setStatusTip(transforms[i].description); + + if (categoryMenus[type].find(category) == categoryMenus[type].end()) { + std::cerr << "WARNING: MainWindow::setupMenus: Internal error: " + << "No category menu for transform \"" + << name.toStdString() << "\" (category = \"" + << category.toStdString() << "\")" << std::endl; + } else { + categoryMenus[type][category]->addAction(action); + } + + if (makerMenus[type].find(maker) == makerMenus[type].end()) { + std::cerr << "WARNING: MainWindow::setupMenus: Internal error: " + << "No maker menu for transform \"" + << name.toStdString() << "\" (maker = \"" + << maker.toStdString() << "\")" << std::endl; + } else { + makerMenus[type][maker]->addAction(action); + } + + action = new QAction(tr("%1...").arg(output == "" ? pluginName : output), this); + connect(action, SIGNAL(triggered()), this, SLOT(addLayer())); + m_transformActions[action] = transforms[i].identifier; + connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool))); + action->setStatusTip(transforms[i].description); + +// cerr << "Transform: \"" << name.toStdString() << "\": plugin name \"" << pluginName.toStdString() << "\"" << endl; + + if (pluginNameMenus[type].find(pluginName) == + pluginNameMenus[type].end()) { + + SubdividingMenu *parentMenu = byPluginNameMenus[type]; + parentMenu->setTearOffEnabled(true); + + if (output == "") { + parentMenu->addAction(pluginName, action); + } else { + pluginNameMenus[type][pluginName] = + parentMenu->addMenu(pluginName); + connect(this, SIGNAL(canAddLayer(bool)), + pluginNameMenus[type][pluginName], + SLOT(setEnabled(bool))); + } + } + + if (pluginNameMenus[type].find(pluginName) != + pluginNameMenus[type].end()) { + pluginNameMenus[type][pluginName]->addAction(action); + } + } + + for (set::iterator i = pendingMenus.begin(); + i != pendingMenus.end(); ++i) { + (*i)->entriesAdded(); + } + + setupRecentTransformsMenu(); +} + +void +MainWindow::setupHelpMenu() +{ + QMenu *menu = menuBar()->addMenu(tr("&Help")); + menu->setTearOffEnabled(true); + + m_keyReference->setCategory(tr("Help")); + + IconLoader il; + + QAction *action = new QAction(il.load("help"), + tr("&Help Reference"), this); + action->setShortcut(tr("F1")); + action->setStatusTip(tr("Open the Sonic Visualiser reference manual")); + connect(action, SIGNAL(triggered()), this, SLOT(help())); + m_keyReference->registerShortcut(action); + menu->addAction(action); + + action = new QAction(tr("&Key and Mouse Reference"), this); + action->setShortcut(tr("F2")); + action->setStatusTip(tr("Open a window showing the keystrokes you can use in Sonic Visualiser")); + connect(action, SIGNAL(triggered()), this, SLOT(keyReference())); + m_keyReference->registerShortcut(action); + 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); +} + +void +MainWindow::setupRecentFilesMenu() +{ + m_recentFilesMenu->clear(); + vector files = m_recentFiles.getRecent(); + for (size_t i = 0; i < files.size(); ++i) { + QAction *action = new QAction(files[i], this); + connect(action, SIGNAL(triggered()), this, SLOT(openRecentFile())); + if (i == 0) { + action->setShortcut(tr("Ctrl+R")); + m_keyReference->registerShortcut + (tr("Re-open"), + action->shortcut(), + tr("Re-open the current or most recently opened file")); + } + m_recentFilesMenu->addAction(action); + } +} + +void +MainWindow::setupRecentTransformsMenu() +{ + m_recentTransformsMenu->clear(); + vector transforms = m_recentTransforms.getRecent(); + for (size_t i = 0; i < transforms.size(); ++i) { + TransformActionReverseMap::iterator ti = + m_transformActionsReverse.find(transforms[i]); + if (ti == m_transformActionsReverse.end()) { + std::cerr << "WARNING: MainWindow::setupRecentTransformsMenu: " + << "Unknown transform \"" << transforms[i].toStdString() + << "\" in recent transforms list" << std::endl; + continue; + } + if (i == 0) { + ti->second->setShortcut(tr("Ctrl+T")); + m_keyReference->registerShortcut + (tr("Repeat Transform"), + ti->second->shortcut(), + tr("Re-select the most recently run transform")); + } + m_recentTransformsMenu->addAction(ti->second); + } +} + +void +MainWindow::setupExistingLayersMenus() +{ + if (!m_existingLayersMenu) return; // should have been created by setupMenus + +// std::cerr << "MainWindow::setupExistingLayersMenu" << std::endl; + + m_existingLayersMenu->clear(); + m_existingLayerActions.clear(); + + m_sliceMenu->clear(); + m_sliceActions.clear(); + + IconLoader il; + + vector orderedLayers; + set observedLayers; + set sliceableLayers; + + LayerFactory *factory = LayerFactory::getInstance(); + + 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); + + if (factory->isLayerSliceable(layer)) { + sliceableLayers.insert(layer); + } + } + } + + map observedNames; + + for (size_t i = 0; i < orderedLayers.size(); ++i) { + + Layer *layer = orderedLayers[i]; + + QString name = layer->getLayerPresentationName(); + int n = ++observedNames[name]; + if (n > 1) name = QString("%1 <%2>").arg(name).arg(n); + + QIcon icon = il.load(factory->getLayerIconName + (factory->getLayerType(layer))); + + QAction *action = new QAction(icon, name, this); + connect(action, SIGNAL(triggered()), this, SLOT(addLayer())); + connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool))); + m_existingLayerActions[action] = layer; + + m_existingLayersMenu->addAction(action); + + if (sliceableLayers.find(layer) != sliceableLayers.end()) { + action = new QAction(icon, name, this); + connect(action, SIGNAL(triggered()), this, SLOT(addLayer())); + connect(this, SIGNAL(canAddLayer(bool)), action, SLOT(setEnabled(bool))); + m_sliceActions[action] = layer; + m_sliceMenu->addAction(action); + } + } + + m_sliceMenu->setEnabled(!m_sliceActions.empty()); +} + +void +MainWindow::setupToolbars() +{ + m_keyReference->setCategory(tr("Playback and Transport Controls")); + + IconLoader il; + + QMenu *menu = m_playbackMenu = menuBar()->addMenu(tr("Play&back")); + menu->setTearOffEnabled(true); + m_rightButtonMenu->addSeparator(); + m_rightButtonPlaybackMenu = m_rightButtonMenu->addMenu(tr("Playback")); + + QToolBar *toolbar = addToolBar(tr("Playback Toolbar")); + + QAction *rwdStartAction = toolbar->addAction(il.load("rewind-start"), + tr("Rewind to Start")); + rwdStartAction->setShortcut(tr("Home")); + rwdStartAction->setStatusTip(tr("Rewind to the start")); + connect(rwdStartAction, SIGNAL(triggered()), this, SLOT(rewindStart())); + connect(this, SIGNAL(canPlay(bool)), rwdStartAction, SLOT(setEnabled(bool))); + + QAction *m_rwdAction = toolbar->addAction(il.load("rewind"), + tr("Rewind")); + m_rwdAction->setShortcut(tr("PgUp")); + m_rwdAction->setStatusTip(tr("Rewind to the previous time instant or time ruler notch")); + connect(m_rwdAction, SIGNAL(triggered()), this, SLOT(rewind())); + connect(this, SIGNAL(canRewind(bool)), m_rwdAction, SLOT(setEnabled(bool))); + + QAction *playAction = toolbar->addAction(il.load("playpause"), + tr("Play / Pause")); + playAction->setCheckable(true); + playAction->setShortcut(tr("Space")); + playAction->setStatusTip(tr("Start or stop playback from the current position")); + connect(playAction, SIGNAL(triggered()), this, SLOT(play())); + connect(m_playSource, SIGNAL(playStatusChanged(bool)), + playAction, SLOT(setChecked(bool))); + connect(this, SIGNAL(canPlay(bool)), playAction, SLOT(setEnabled(bool))); + + m_ffwdAction = toolbar->addAction(il.load("ffwd"), + tr("Fast Forward")); + m_ffwdAction->setShortcut(tr("PgDown")); + m_ffwdAction->setStatusTip(tr("Fast-forward to the next time instant or time ruler notch")); + connect(m_ffwdAction, SIGNAL(triggered()), this, SLOT(ffwd())); + connect(this, SIGNAL(canFfwd(bool)), m_ffwdAction, SLOT(setEnabled(bool))); + + QAction *ffwdEndAction = toolbar->addAction(il.load("ffwd-end"), + tr("Fast Forward to End")); + ffwdEndAction->setShortcut(tr("End")); + ffwdEndAction->setStatusTip(tr("Fast-forward to the end")); + connect(ffwdEndAction, SIGNAL(triggered()), this, SLOT(ffwdEnd())); + connect(this, SIGNAL(canPlay(bool)), ffwdEndAction, SLOT(setEnabled(bool))); + + toolbar = addToolBar(tr("Play Mode Toolbar")); + + QAction *psAction = toolbar->addAction(il.load("playselection"), + tr("Constrain Playback to Selection")); + psAction->setCheckable(true); + psAction->setChecked(m_viewManager->getPlaySelectionMode()); + psAction->setShortcut(tr("s")); + psAction->setStatusTip(tr("Constrain playback to the selected regions")); + connect(m_viewManager, SIGNAL(playSelectionModeChanged(bool)), + psAction, SLOT(setChecked(bool))); + connect(psAction, SIGNAL(triggered()), this, SLOT(playSelectionToggled())); + connect(this, SIGNAL(canPlaySelection(bool)), psAction, SLOT(setEnabled(bool))); + + QAction *plAction = toolbar->addAction(il.load("playloop"), + tr("Loop Playback")); + plAction->setCheckable(true); + plAction->setChecked(m_viewManager->getPlayLoopMode()); + plAction->setShortcut(tr("l")); + plAction->setStatusTip(tr("Loop playback")); + connect(m_viewManager, SIGNAL(playLoopModeChanged(bool)), + plAction, SLOT(setChecked(bool))); + connect(plAction, SIGNAL(triggered()), this, SLOT(playLoopToggled())); + connect(this, SIGNAL(canPlay(bool)), plAction, SLOT(setEnabled(bool))); + + QAction *soAction = toolbar->addAction(il.load("solo"), + tr("Solo Current Pane")); + soAction->setCheckable(true); + soAction->setChecked(m_viewManager->getPlaySoloMode()); + soAction->setShortcut(tr("o")); + soAction->setStatusTip(tr("Solo the current pane during playback")); + connect(m_viewManager, SIGNAL(playSoloModeChanged(bool)), + soAction, SLOT(setChecked(bool))); + connect(soAction, SIGNAL(triggered()), this, SLOT(playSoloToggled())); + connect(this, SIGNAL(canPlay(bool)), soAction, SLOT(setEnabled(bool))); + + m_keyReference->registerShortcut(playAction); + m_keyReference->registerShortcut(psAction); + m_keyReference->registerShortcut(plAction); + m_keyReference->registerShortcut(soAction); + m_keyReference->registerShortcut(m_rwdAction); + m_keyReference->registerShortcut(m_ffwdAction); + m_keyReference->registerShortcut(rwdStartAction); + m_keyReference->registerShortcut(ffwdEndAction); + + menu->addAction(playAction); + menu->addAction(psAction); + menu->addAction(plAction); + menu->addAction(soAction); + menu->addSeparator(); + menu->addAction(m_rwdAction); + menu->addAction(m_ffwdAction); + menu->addSeparator(); + menu->addAction(rwdStartAction); + menu->addAction(ffwdEndAction); + menu->addSeparator(); + + m_rightButtonPlaybackMenu->addAction(playAction); + m_rightButtonPlaybackMenu->addAction(psAction); + m_rightButtonPlaybackMenu->addAction(plAction); + m_rightButtonPlaybackMenu->addAction(soAction); + m_rightButtonPlaybackMenu->addSeparator(); + m_rightButtonPlaybackMenu->addAction(m_rwdAction); + m_rightButtonPlaybackMenu->addAction(m_ffwdAction); + m_rightButtonPlaybackMenu->addSeparator(); + m_rightButtonPlaybackMenu->addAction(rwdStartAction); + m_rightButtonPlaybackMenu->addAction(ffwdEndAction); + m_rightButtonPlaybackMenu->addSeparator(); + + QAction *fastAction = menu->addAction(tr("Speed Up")); + fastAction->setShortcut(tr("Ctrl+PgUp")); + fastAction->setStatusTip(tr("Time-stretch playback to speed it up without changing pitch")); + connect(fastAction, SIGNAL(triggered()), this, SLOT(speedUpPlayback())); + connect(this, SIGNAL(canSpeedUpPlayback(bool)), fastAction, SLOT(setEnabled(bool))); + + QAction *slowAction = menu->addAction(tr("Slow Down")); + slowAction->setShortcut(tr("Ctrl+PgDown")); + slowAction->setStatusTip(tr("Time-stretch playback to slow it down without changing pitch")); + connect(slowAction, SIGNAL(triggered()), this, SLOT(slowDownPlayback())); + connect(this, SIGNAL(canSlowDownPlayback(bool)), slowAction, SLOT(setEnabled(bool))); + + QAction *normalAction = menu->addAction(tr("Restore Normal Speed")); + normalAction->setShortcut(tr("Ctrl+Home")); + normalAction->setStatusTip(tr("Restore non-time-stretched playback")); + connect(normalAction, SIGNAL(triggered()), this, SLOT(restoreNormalPlayback())); + connect(this, SIGNAL(canChangePlaybackSpeed(bool)), normalAction, SLOT(setEnabled(bool))); + + m_keyReference->registerShortcut(fastAction); + m_keyReference->registerShortcut(slowAction); + m_keyReference->registerShortcut(normalAction); + + m_rightButtonPlaybackMenu->addAction(fastAction); + m_rightButtonPlaybackMenu->addAction(slowAction); + m_rightButtonPlaybackMenu->addAction(normalAction); + + toolbar = addToolBar(tr("Edit Toolbar")); + CommandHistory::getInstance()->registerToolbar(toolbar); + +/*!!! + m_keyReference->setCategory(tr("Tool Selection")); + + toolbar = addToolBar(tr("Tools Toolbar")); + QActionGroup *group = new QActionGroup(this); + + QAction *action = toolbar->addAction(il.load("navigate"), + tr("Navigate")); + action->setCheckable(true); + action->setChecked(true); + action->setShortcut(tr("1")); + action->setStatusTip(tr("Navigate")); + connect(action, SIGNAL(triggered()), this, SLOT(toolNavigateSelected())); + group->addAction(action); + m_keyReference->registerShortcut(action); + m_toolActions[ViewManager::NavigateMode] = action; + + action = toolbar->addAction(il.load("select"), + tr("Select")); + action->setCheckable(true); + action->setShortcut(tr("2")); + action->setStatusTip(tr("Select ranges")); + connect(action, SIGNAL(triggered()), this, SLOT(toolSelectSelected())); + group->addAction(action); + m_keyReference->registerShortcut(action); + m_toolActions[ViewManager::SelectMode] = action; + + action = toolbar->addAction(il.load("move"), + tr("Edit")); + action->setCheckable(true); + action->setShortcut(tr("3")); + action->setStatusTip(tr("Edit items in layer")); + connect(action, SIGNAL(triggered()), this, SLOT(toolEditSelected())); + connect(this, SIGNAL(canEditLayer(bool)), action, SLOT(setEnabled(bool))); + group->addAction(action); + m_keyReference->registerShortcut(action); + m_toolActions[ViewManager::EditMode] = action; + + action = toolbar->addAction(il.load("draw"), + tr("Draw")); + action->setCheckable(true); + action->setShortcut(tr("4")); + action->setStatusTip(tr("Draw new items in layer")); + connect(action, SIGNAL(triggered()), this, SLOT(toolDrawSelected())); + connect(this, SIGNAL(canEditLayer(bool)), action, SLOT(setEnabled(bool))); + group->addAction(action); + m_keyReference->registerShortcut(action); + m_toolActions[ViewManager::DrawMode] = action; + + action = toolbar->addAction(il.load("measure"), + tr("Measure")); + action->setCheckable(true); + action->setShortcut(tr("5")); + action->setStatusTip(tr("Make measurements in layer")); + connect(action, SIGNAL(triggered()), this, SLOT(toolMeasureSelected())); + connect(this, SIGNAL(canMeasureLayer(bool)), action, SLOT(setEnabled(bool))); + group->addAction(action); + m_keyReference->registerShortcut(action); + m_toolActions[ViewManager::MeasureMode] = action; + +// action = toolbar->addAction(il.load("text"), +// 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(); + + Pane::registerShortcuts(*m_keyReference); +} + +void +MainWindow::updateMenuStates() +{ + Pane *currentPane = 0; + Layer *currentLayer = 0; + + if (m_paneStack) currentPane = m_paneStack->getCurrentPane(); + if (currentPane) currentLayer = currentPane->getSelectedLayer(); + + bool haveCurrentPane = + (currentPane != 0); + bool haveCurrentLayer = + (haveCurrentPane && + (currentLayer != 0)); + bool haveMainModel = + (getMainModel() != 0); + bool havePlayTarget = + (m_playTarget != 0); + bool haveSelection = + (m_viewManager && + !m_viewManager->getSelections().empty()); + bool haveCurrentEditableLayer = + (haveCurrentLayer && + currentLayer->isLayerEditable()); + bool haveCurrentTimeInstantsLayer = + (haveCurrentLayer && + dynamic_cast(currentLayer)); + bool haveCurrentTimeValueLayer = + (haveCurrentLayer && + dynamic_cast(currentLayer)); + bool haveCurrentColour3DPlot = + (haveCurrentLayer && + dynamic_cast(currentLayer)); + 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 canExportImage(haveMainModel && haveCurrentPane); + emit canDeleteCurrentLayer(haveCurrentLayer); + emit canRenameLayer(haveCurrentLayer); + emit canEditLayer(haveCurrentEditableLayer); + emit canMeasureLayer(haveCurrentLayer); + emit canSelect(haveMainModel && haveCurrentPane); + emit canPlay(havePlayTarget); + emit canFfwd(true); + emit canRewind(true); + emit canPaste(haveCurrentEditableLayer && haveClipboardContents); + emit canInsertInstant(haveCurrentPane); + emit canInsertInstantsAtBoundaries(haveCurrentPane && haveSelection); + emit canPlaySelection(haveMainModel && havePlayTarget && haveSelection); + emit canClearSelection(haveSelection); + emit canEditSelection(haveSelection && haveCurrentEditableLayer); + emit canSave(m_sessionFile != "" && m_documentModified); + + if (m_deleteSelectedAction) { + if (m_viewManager && + (m_viewManager->getToolMode() == ViewManager::MeasureMode)) { + emit canDeleteSelection(haveCurrentLayer); + m_deleteSelectedAction->setText(tr("&Delete Current Measurement")); + m_deleteSelectedAction->setStatusTip(tr("Delete the measurement currently under the mouse pointer")); + } else { + emit canDeleteSelection(haveSelection && haveCurrentEditableLayer); + m_deleteSelectedAction->setText(tr("&Delete Selected Items")); + m_deleteSelectedAction->setStatusTip(tr("Delete items in current selection from the current layer")); + } + } + + emit canChangePlaybackSpeed(true); + int v = m_playSpeed->value(); + emit canSpeedUpPlayback(v < m_playSpeed->maximum()); + emit canSlowDownPlayback(v > m_playSpeed->minimum()); + + if (m_ffwdAction && m_rwdAction) { + if (haveCurrentTimeInstantsLayer) { + m_ffwdAction->setText(tr("Fast Forward to Next Instant")); + m_ffwdAction->setStatusTip(tr("Fast forward to the next time instant in the current layer")); + m_rwdAction->setText(tr("Rewind to Previous Instant")); + m_rwdAction->setStatusTip(tr("Rewind to the previous time instant in the current layer")); + } else if (haveCurrentTimeValueLayer) { + m_ffwdAction->setText(tr("Fast Forward to Next Point")); + m_ffwdAction->setStatusTip(tr("Fast forward to the next point in the current layer")); + m_rwdAction->setText(tr("Rewind to Previous Point")); + m_rwdAction->setStatusTip(tr("Rewind to the previous point in the current layer")); + } else { + m_ffwdAction->setText(tr("Fast Forward")); + m_ffwdAction->setStatusTip(tr("Fast forward")); + m_rwdAction->setText(tr("Rewind")); + m_rwdAction->setStatusTip(tr("Rewind")); + } + } +} + +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::playSoloToggled() +{ + QAction *action = dynamic_cast(sender()); + + if (action) { + m_viewManager->setPlaySoloMode(action->isChecked()); + } else { + m_viewManager->setPlaySoloMode(!m_viewManager->getPlaySoloMode()); + } + + if (!m_viewManager->getPlaySoloMode()) { + m_viewManager->setPlaybackModel(0); + if (m_playSource) { + m_playSource->clearSoloModelSet(); + } + } +} + +void +MainWindow::currentPaneChanged(Pane *p) +{ + updateMenuStates(); + updateVisibleRangeDisplay(p); + + if (!p) return; + + if (!(m_viewManager && + m_playSource && + m_viewManager->getPlaySoloMode())) { + if (m_viewManager) m_viewManager->setPlaybackModel(0); + return; + } + + Model *prevPlaybackModel = m_viewManager->getPlaybackModel(); + + std::set soloModels; + + for (int i = 0; i < p->getLayerCount(); ++i) { + Layer *layer = p->getLayer(i); + if (dynamic_cast(layer)) { + continue; + } + if (layer && layer->getModel()) { + Model *model = layer->getModel(); + if (dynamic_cast(model)) { + m_viewManager->setPlaybackModel(model); + } + soloModels.insert(model); + } + } + + RangeSummarisableTimeValueModel *a = + dynamic_cast(prevPlaybackModel); + RangeSummarisableTimeValueModel *b = + dynamic_cast(m_viewManager-> + getPlaybackModel()); + + m_playSource->setSoloModelSet(soloModels); + + if (a && b && (a != b)) { + int frame = m_playSource->getCurrentPlayingFrame(); + //!!! I don't really believe that these functions are the right way around + int rframe = a->alignFromReference(frame); + int bframe = b->alignToReference(rframe); + if (m_playSource->isPlaying()) m_playSource->play(bframe); + } +} + +void +MainWindow::currentLayerChanged(Pane *p, Layer *) +{ + updateMenuStates(); + updateVisibleRangeDisplay(p); +} + +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::toolMeasureSelected() +{ + m_viewManager->setToolMode(ViewManager::MeasureMode); +} + +//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()) { + + Layer *layer = m_paneStack->getCurrentPane()->getSelectedLayer(); + + if (m_viewManager && + (m_viewManager->getToolMode() == ViewManager::MeasureMode)) { + + layer->deleteCurrentMeasureRect(); + + } else { + + MultiSelection::SelectionList selections = + m_viewManager->getSelections(); + + for (MultiSelection::SelectionList::iterator i = selections.begin(); + i != selections.end(); ++i) { + layer->deleteSelection(*i); + } + } + } +} + +void +MainWindow::insertInstant() +{ + int frame = m_viewManager->getPlaybackFrame(); + insertInstantAt(frame); +} + +void +MainWindow::insertInstantsAtBoundaries() +{ + MultiSelection::SelectionList selections = m_viewManager->getSelections(); + for (MultiSelection::SelectionList::iterator i = selections.begin(); + i != selections.end(); ++i) { + size_t start = i->getStartFrame(); + size_t end = i->getEndFrame(); + if (start != end) { + insertInstantAt(i->getStartFrame()); + insertInstantAt(i->getEndFrame()); + } + } +} + +void +MainWindow::insertInstantAt(size_t frame) +{ + 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 path = getOpenFileName(FileFinder::AudioFile); + + if (path != "") { + if (openAudioFile(path, ReplaceMainModel) == FileOpenFailed) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("Audio file \"%1\" could not be opened").arg(path)); + } + } +} + +void +MainWindow::importMoreAudio() +{ + QString path = getOpenFileName(FileFinder::AudioFile); + + if (path != "") { + if (openAudioFile(path, CreateAdditionalModel) == FileOpenFailed) { + 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 = getSaveFileName(FileFinder::AudioFile); + + if (path == "") return; + + bool ok = false; + QString error; + + MultiSelection ms = m_viewManager->getSelection(); + MultiSelection::SelectionList selections = m_viewManager->getSelections(); + + bool multiple = false; + + MultiSelection *selectionToWrite = 0; + + 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]) selectionToWrite = &ms; + + } else if (selections.size() > 1) { + + 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"); + + 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]) { + + selectionToWrite = &ms; + + } else if (item == items[1]) { + + 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()->getChannelCount()); + subwriter.writeModel(getMainModel(), &subms); + ok = subwriter.isOK(); + + if (!ok) { + error = subwriter.getError(); + break; + } + } + } + } + + if (!multiple) { + WavFileWriter writer(path, + getMainModel()->getSampleRate(), + getMainModel()->getChannelCount()); + writer.writeModel(getMainModel(), selectionToWrite); + ok = writer.isOK(); + error = writer.getError(); + } + + if (ok) { + if (!multiple) { + m_recentFiles.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 = getOpenFileName(FileFinder::LayerFile); + + if (path != "") { + + if (openLayerFile(path) == FileOpenFailed) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("File %1 could not be opened.").arg(path)); + return; + } + } +} + +MainWindow::FileOpenStatus +MainWindow::openLayerFile(QString path) +{ + return openLayerFile(path, path); +} + +MainWindow::FileOpenStatus +MainWindow::openLayerFile(QString path, QString location) +{ + 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 FileOpenFailed; + } + + 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 FileOpenFailed; + } + + bool realFile = (location == path); + + 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(" + << location.toStdString() + << "): Failed to open file for reading" << std::endl; + return FileOpenFailed; + } + + SVFileReader reader(m_document, callback, location); + reader.setCurrentPane(pane); + + QXmlInputSource inputSource(&file); + reader.parse(inputSource); + + if (!reader.isOK()) { + std::cerr << "ERROR: MainWindow::openLayerFile(" + << location.toStdString() + << "): Failed to read XML file: " + << reader.getErrorString().toStdString() << std::endl; + return FileOpenFailed; + } + + m_recentFiles.addFile(location); + + if (realFile) { + registerLastOpenedFilePath(FileFinder::LayerFile, path); // for file dialog + } + + return FileOpenSucceeded; + + } else { + + Model *model = DataFileReaderFactory::load(path, getMainModel()->getSampleRate()); + + if (model) { + + Layer *newLayer = m_document->createImportedLayer(model); + + if (newLayer) { + + m_document->addLayerToView(pane, newLayer); + m_recentFiles.addFile(location); + + if (realFile) { + registerLastOpenedFilePath(FileFinder::LayerFile, path); // for file dialog + } + + return FileOpenSucceeded; + } + } + } + + return FileOpenFailed; +} + +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 = getSaveFileName(FileFinder::LayerFile); + + 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 { + m_recentFiles.addFile(path); + } +} + +void +MainWindow::exportImage() +{ + Pane *pane = m_paneStack->getCurrentPane(); + if (!pane) return; + + QString path = getSaveFileName(FileFinder::ImageFile); + + if (path == "") return; + + if (QFileInfo(path).suffix() == "") path += ".png"; + + bool haveSelection = m_viewManager && !m_viewManager->getSelections().empty(); + + QSize total, visible, selected; + total = pane->getImageSize(); + visible = pane->getImageSize(pane->getFirstVisibleFrame(), + pane->getLastVisibleFrame()); + + size_t sf0 = 0, sf1 = 0; + + if (haveSelection) { + MultiSelection::SelectionList selections = m_viewManager->getSelections(); + sf0 = selections.begin()->getStartFrame(); + MultiSelection::SelectionList::iterator e = selections.end(); + --e; + sf1 = e->getEndFrame(); + selected = pane->getImageSize(sf0, sf1); + } + + QStringList items; + items << tr("Export the whole pane (%1x%2 pixels)") + .arg(total.width()).arg(total.height()); + items << tr("Export the visible area only (%1x%2 pixels)") + .arg(visible.width()).arg(visible.height()); + if (haveSelection) { + items << tr("Export the selection extent (%1x%2 pixels)") + .arg(selected.width()).arg(selected.height()); + } else { + items << tr("Export the selection extent"); + } + + QSettings settings; + settings.beginGroup("MainWindow"); + int deflt = settings.value("lastimageexportregion", 0).toInt(); + if (deflt == 2 && !haveSelection) deflt = 1; + if (deflt == 0 && total.width() > 32767) deflt = 1; + + ListInputDialog *lid = new ListInputDialog + (this, tr("Select region to export"), + tr("Which region of the current pane do you want to export as an image?"), + items, deflt); + + if (!haveSelection) { + lid->setItemAvailability(2, false); + } + if (total.width() > 32767) { // appears to be the limit of a QImage + lid->setItemAvailability(0, false); + lid->setFootnote(tr("Note: the whole pane is too wide to be exported as a single image.")); + } + + bool ok = lid->exec(); + QString item = lid->getCurrentString(); + delete lid; + + if (!ok || item.isEmpty()) return; + + settings.setValue("lastimageexportregion", deflt); + + QImage *image = 0; + + if (item == items[0]) { + image = pane->toNewImage(); + } else if (item == items[1]) { + image = pane->toNewImage(pane->getFirstVisibleFrame(), + pane->getLastVisibleFrame()); + } else if (haveSelection) { + image = pane->toNewImage(sf0, sf1); + } + + if (!image) return; + + if (!image->save(path, "PNG")) { + QMessageBox::critical(this, tr("Failed to save image file"), + tr("Failed to save image file %1").arg(path)); + } + + delete image; +} + +MainWindow::FileOpenStatus +MainWindow::openAudioFile(QString path, AudioFileOpenMode mode) +{ + return openAudioFile(path, path, mode); +} + +MainWindow::FileOpenStatus +MainWindow::openAudioFile(QString path, QString location, AudioFileOpenMode mode) +{ + if (!(QFileInfo(path).exists() && + QFileInfo(path).isFile() && + QFileInfo(path).isReadable())) { + return FileOpenFailed; + } + + m_openingAudioFile = true; + + size_t rate = 0; + + if (Preferences::getInstance()->getResampleOnLoad()) { + rate = m_playSource->getSourceSampleRate(); + } + + WaveFileModel *newModel = new WaveFileModel(path, location, rate); + + if (!newModel->isOK()) { + delete newModel; + m_openingAudioFile = false; + return FileOpenFailed; + } + + bool setAsMain = true; + static bool prevSetAsMain = true; + + bool realFile = (location == path); + + //!!! + mode = CreateAdditionalModel; + + if (mode == CreateAdditionalModel) { + if (m_document->getMainModel()) { + 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; + m_openingAudioFile = false; + return FileOpenCancelled; + } + + setAsMain = (item == items[0]); + prevSetAsMain = setAsMain; + } + } + + if (setAsMain) { + + Model *prevMain = getMainModel(); + if (prevMain) { + m_playSource->removeModel(prevMain); + PlayParameterRepository::getInstance()->removeModel(prevMain); + } + + PlayParameterRepository::getInstance()->addModel(newModel); + + m_document->setMainModel(newModel); + setupMenus(); + + if (m_sessionFile == "") { + setWindowTitle(tr("Vect: %1") + .arg(QFileInfo(location).fileName())); + CommandHistory::getInstance()->clear(); + CommandHistory::getInstance()->documentSaved(); + m_documentModified = false; + } else { + setWindowTitle(tr("Vect: %1 [%2]") + .arg(QFileInfo(m_sessionFile).fileName()) + .arg(QFileInfo(location).fileName())); + if (m_documentModified) { + m_documentModified = false; + documentModified(); // so as to restore "(modified)" window title + } + } + + if (realFile) m_audioFile = path; + + } else { // !setAsMain + + CommandHistory::getInstance()->startCompoundOperation + (tr("Import \"%1\"").arg(QFileInfo(location).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(); + m_recentFiles.addFile(location); + if (realFile) { + registerLastOpenedFilePath(FileFinder::AudioFile, path); // for file dialog + } + m_openingAudioFile = false; + + currentPaneChanged(m_paneStack->getCurrentPane()); + + return FileOpenSucceeded; +} + +MainWindow::FileOpenStatus +MainWindow::openPlaylistFile(QString path, AudioFileOpenMode mode) +{ + return openPlaylistFile(path, path, mode); +} + +MainWindow::FileOpenStatus +MainWindow::openPlaylistFile(QString path, QString location, AudioFileOpenMode mode) +{ + if (!(QFileInfo(path).exists() && + QFileInfo(path).isFile() && + QFileInfo(path).isReadable())) { + return FileOpenFailed; + } + + std::set extensions; + PlaylistFileReader::getSupportedExtensions(extensions); + QString extension = QFileInfo(path).suffix(); + if (extensions.find(extension) == extensions.end()) return FileOpenFailed; + + PlaylistFileReader reader(path); + if (!reader.isOK()) return FileOpenFailed; + + PlaylistFileReader::Playlist playlist = reader.load(); + + bool someSuccess = false; + + for (PlaylistFileReader::Playlist::const_iterator i = playlist.begin(); + i != playlist.end(); ++i) { + + FileOpenStatus status = openURL(*i, mode); + + if (status == FileOpenCancelled) { + return FileOpenCancelled; + } + + if (status == FileOpenSucceeded) { + someSuccess = true; + mode = CreateAdditionalModel; + } + } + + if (someSuccess) return FileOpenSucceeded; + else return FileOpenFailed; +} + +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); + } + connect(m_fader, SIGNAL(valueChanged(float)), + m_playTarget, SLOT(setOutputGain(float))); +} + +WaveFileModel * +MainWindow::getMainModel() +{ + if (!m_document) return 0; + return m_document->getMainModel(); +} + +const WaveFileModel * +MainWindow::getMainModel() const +{ + if (!m_document) return 0; + return m_document->getMainModel(); +} + +void +MainWindow::newSession() +{ + if (!checkSaveModified()) return; + + closeSession(); + createDocument(); + + Pane *pane = m_paneStack->addPane(); + + connect(pane, SIGNAL(contextHelpChanged(const QString &)), + this, SLOT(contextHelpChanged(const QString &))); + + 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_overview->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, QString)), + this, SLOT(modelRegenerationFailed(QString, 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_overview->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_overview->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("Vect")); + + 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 = getOpenFileName(FileFinder::SessionFile); + + if (path.isEmpty()) return; + + if (openSessionFile(path) == FileOpenFailed) { + 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 path = getOpenFileName(FileFinder::AnyFile); + + if (path.isEmpty()) return; + + if (path.endsWith(".sv")) { + + if (!checkSaveModified()) return; + + if (openSessionFile(path) == FileOpenFailed) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("Session file \"%1\" could not be opened").arg(path)); + } + + } else { + + if (openPlaylistFile(path, AskUser) == FileOpenFailed) { + + if (openAudioFile(path, AskUser) == FileOpenFailed) { + + if (!canImportLayer || (openLayerFile(path) == FileOpenFailed)) { + + QMessageBox::critical(this, tr("Failed to open file"), + tr("File \"%1\" could not be opened").arg(path)); + } + } + } + } +} + +void +MainWindow::openLocation() +{ + QSettings settings; + settings.beginGroup("MainWindow"); + QString lastLocation = settings.value("lastremote", "").toString(); + + bool ok = false; + QString text = QInputDialog::getText + (this, tr("Open Location"), + tr("Please enter the URL of the location to open:"), + QLineEdit::Normal, lastLocation, &ok); + + if (!ok) return; + + settings.setValue("lastremote", text); + + if (text.isEmpty()) return; + + if (openURL(QUrl(text)) == FileOpenFailed) { + QMessageBox::critical(this, tr("Failed to open location"), + tr("URL \"%1\" could not be opened").arg(text)); + } +} + +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; + + QUrl url(path); + if (RemoteFile::canHandleScheme(url)) { + openURL(url); + return; + } + + if (path.endsWith("sv")) { + + if (!checkSaveModified()) return; + + if (openSessionFile(path) == FileOpenFailed) { + QMessageBox::critical(this, tr("Failed to open file"), + tr("Session file \"%1\" could not be opened").arg(path)); + } + + } else { + + if (openPlaylistFile(path, AskUser) == FileOpenFailed) { + + if (openAudioFile(path, AskUser) == FileOpenFailed) { + + bool canImportLayer = (getMainModel() != 0 && + m_paneStack != 0 && + m_paneStack->getCurrentPane() != 0); + + if (!canImportLayer || (openLayerFile(path) == FileOpenFailed)) { + + QMessageBox::critical(this, tr("Failed to open file"), + tr("File \"%1\" could not be opened").arg(path)); + } + } + } + } +} + +MainWindow::FileOpenStatus +MainWindow::openURL(QUrl url, AudioFileOpenMode mode) +{ + if (url.scheme().toLower() == "file") { + + return openSomeFile(url.toLocalFile(), mode); + + } else if (!RemoteFile::canHandleScheme(url)) { + + QMessageBox::critical(this, tr("Unsupported scheme in URL"), + tr("The URL scheme \"%1\" is not supported") + .arg(url.scheme())); + return FileOpenFailed; + + } else { + RemoteFile rf(url); + rf.wait(); + if (!rf.isOK()) { + QMessageBox::critical(this, tr("File download failed"), + tr("Failed to download URL \"%1\": %2") + .arg(url.toString()).arg(rf.getErrorString())); + return FileOpenFailed; + } + FileOpenStatus status; + if ((status = openSomeFile(rf.getLocalFilename(), url.toString(), + mode)) != + FileOpenSucceeded) { + rf.deleteLocalFile(); + } + return status; + } +} + +MainWindow::FileOpenStatus +MainWindow::openURL(QString ustr, AudioFileOpenMode mode) +{ + // This function is used when we don't know whether the string is + // an encoded or human-readable url + + QUrl url(ustr); + + if (url.scheme().toLower() == "file") { + + return openSomeFile(url.toLocalFile(), mode); + + } else if (!RemoteFile::canHandleScheme(url)) { + + QMessageBox::critical(this, tr("Unsupported scheme in URL"), + tr("The URL scheme \"%1\" is not supported") + .arg(url.scheme())); + return FileOpenFailed; + + } else { + RemoteFile rf(url); + rf.wait(); + if (!rf.isOK()) { + // rf was created on the assumption that ustr was + // human-readable. Let's try again, this time assuming it + // was already encoded. + std::cerr << "MainWindow::openURL: Failed to retrieve URL \"" + << ustr.toStdString() << "\" as human-readable URL; " + << "trying again treating it as encoded URL" + << std::endl; + url.setEncodedUrl(ustr.toAscii()); + return openURL(url, mode); + } + + FileOpenStatus status; + if ((status = openSomeFile(rf.getLocalFilename(), ustr, mode)) != + FileOpenSucceeded) { + rf.deleteLocalFile(); + } + return status; + } +} + +MainWindow::FileOpenStatus +MainWindow::openSomeFile(QString path, AudioFileOpenMode mode) +{ + return openSomeFile(path, path, mode); +} + +MainWindow::FileOpenStatus +MainWindow::openSomeFile(QString path, QString location, + AudioFileOpenMode mode) +{ + FileOpenStatus status; + + bool canImportLayer = (getMainModel() != 0 && + m_paneStack != 0 && + m_paneStack->getCurrentPane() != 0); + + if ((status = openPlaylistFile(path, location, mode)) != FileOpenFailed) { + return status; + } else if ((status = openAudioFile(path, location, mode)) != FileOpenFailed) { + return status; + } else if ((status = openSessionFile(path, location)) != FileOpenFailed) { + return status; + } else if (!canImportLayer) { + return FileOpenFailed; + } else if ((status = openLayerFile(path, location)) != FileOpenFailed) { + return status; + } else { + return FileOpenFailed; + } +} + +MainWindow::FileOpenStatus +MainWindow::openSessionFile(QString path) +{ + return openSessionFile(path, path); +} + +MainWindow::FileOpenStatus +MainWindow::openSessionFile(QString path, QString location) +{ + BZipFileDevice bzFile(path); + if (!bzFile.open(QIODevice::ReadOnly)) { + std::cerr << "Failed to open session file \"" << location.toStdString() + << "\": " << bzFile.errorString().toStdString() << std::endl; + return FileOpenFailed; + } + + if (!checkSaveModified()) return FileOpenCancelled; + + QString error; + closeSession(); + createDocument(); + + PaneCallback callback(this); + m_viewManager->clearSelections(); + + SVFileReader reader(m_document, callback, location); + 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 == ""); + + bool realFile = (location == path); + + if (ok) { + + setWindowTitle(tr("Vect: %1") + .arg(QFileInfo(location).fileName())); + + if (realFile) m_sessionFile = path; + + setupMenus(); + CommandHistory::getInstance()->clear(); + CommandHistory::getInstance()->documentSaved(); + m_documentModified = false; + updateMenuStates(); + + m_recentFiles.addFile(location); + + if (realFile) { + registerLastOpenedFilePath(FileFinder::SessionFile, path); // for file dialog + } + + } else { + setWindowTitle(tr("Vect")); + } + + return ok ? FileOpenSucceeded : FileOpenFailed; +} + +void +MainWindow::closeEvent(QCloseEvent *e) +{ +// std::cerr << "MainWindow::closeEvent" << std::endl; + + if (m_openingAudioFile) { +// std::cerr << "Busy - ignoring close event" << std::endl; + e->ignore(); + return; + } + + if (!m_abandoning && !checkSaveModified()) { +// std::cerr << "Ignoring close event" << std::endl; + e->ignore(); + return; + } + + QSettings settings; + settings.beginGroup("MainWindow"); + settings.setValue("size", size()); + settings.setValue("position", pos()); + settings.endGroup(); + + delete m_keyReference; + m_keyReference = 0; + + if (m_preferencesDialog && + m_preferencesDialog->isVisible()) { + closeSession(); // otherwise we'll have to wait for prefs changes + m_preferencesDialog->applicationClosing(false); + } + + if (m_layerTreeView && + m_layerTreeView->isVisible()) { + delete m_layerTreeView; + } + + e->accept(); + return; +} + +bool +MainWindow::commitData(bool mayAskUser) +{ + if (mayAskUser) { + bool rv = checkSaveModified(); + if (rv) { + if (m_preferencesDialog && + m_preferencesDialog->isVisible()) { + m_preferencesDialog->applicationClosing(false); + } + } + return rv; + } else { + if (m_preferencesDialog && + m_preferencesDialog->isVisible()) { + m_preferencesDialog->applicationClosing(true); + } + if (!m_documentModified) return true; + + // If we can't check with the user first, then we can't save + // to the original session file (even if we have it) -- have + // to use a temporary file + + QString svDirBase = ".sv1"; + QString svDir = QDir::home().filePath(svDirBase); + + if (!QFileInfo(svDir).exists()) { + if (!QDir::home().mkdir(svDirBase)) return false; + } else { + if (!QFileInfo(svDir).isDir()) return false; + } + + // This name doesn't have to be unguessable +#ifndef _WIN32 + QString fname = QString("tmp-%1-%2.sv") + .arg(QDateTime::currentDateTime().toString("yyyyMMddhhmmsszzz")) + .arg(QProcess().pid()); +#else + QString fname = QString("tmp-%1.sv") + .arg(QDateTime::currentDateTime().toString("yyyyMMddhhmmsszzz")); +#endif + QString fpath = QDir(svDir).filePath(fname); + if (saveSessionFile(fpath)) { + m_recentFiles.addFile(fpath); + return true; + } else { + return false; + } + } +} + +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, + QMessageBox::Yes); + + 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 = getSaveFileName(FileFinder::SessionFile); + + if (path == "") return; + + if (!saveSessionFile(path)) { + QMessageBox::critical(this, tr("Failed to save file"), + tr("Session file \"%1\" could not be saved.").arg(path)); + } else { + setWindowTitle(tr("Vect: %1") + .arg(QFileInfo(path).fileName())); + m_sessionFile = path; + CommandHistory::getInstance()->documentSaved(); + documentRestored(); + m_recentFiles.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.isOK()) { + 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 sw = currentPane->getVerticalScaleWidth(); + if (pixels > sw * 2) pixels -= sw * 2; + else pixels = 1; + if (pixels > 4) pixels -= 4; + + size_t zoomLevel = (end - start) / pixels; + + currentPane->setZoomLevel(zoomLevel); + currentPane->setCentreFrame((start + end) / 2); +} + +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::showMinimalOverlays() +{ + m_viewManager->setOverlayMode(ViewManager::MinimalOverlays); +} + +void +MainWindow::showStandardOverlays() +{ + m_viewManager->setOverlayMode(ViewManager::StandardOverlays); +} + +void +MainWindow::showAllOverlays() +{ + m_viewManager->setOverlayMode(ViewManager::AllOverlays); +} + +void +MainWindow::toggleZoomWheels() +{ + if (m_viewManager->getZoomWheelsEnabled()) { + m_viewManager->setZoomWheelsEnabled(false); + } else { + m_viewManager->setZoomWheelsEnabled(true); + } +} + +void +MainWindow::togglePropertyBoxes() +{ + if (m_paneStack->getLayoutStyle() == PaneStack::NoPropertyStacks) { + if (Preferences::getInstance()->getPropertyBoxLayout() == + Preferences::VerticallyStacked) { + m_paneStack->setLayoutStyle(PaneStack::PropertyStackPerPaneLayout); + } else { + m_paneStack->setLayoutStyle(PaneStack::SinglePropertyStackLayout); + } + } else { + m_paneStack->setLayoutStyle(PaneStack::NoPropertyStacks); + } +} + +void +MainWindow::toggleStatusBar() +{ + QSettings settings; + settings.beginGroup("MainWindow"); + bool sb = settings.value("showstatusbar", true).toBool(); + + if (sb) { + statusBar()->hide(); + } else { + statusBar()->show(); + } + + settings.setValue("showstatusbar", !sb); + + settings.endGroup(); +} + +void +MainWindow::preferenceChanged(PropertyContainer::PropertyName name) +{ + if (name == "Property Box Layout") { + if (m_paneStack->getLayoutStyle() != PaneStack::NoPropertyStacks) { + if (Preferences::getInstance()->getPropertyBoxLayout() == + Preferences::VerticallyStacked) { + m_paneStack->setLayoutStyle(PaneStack::PropertyStackPerPaneLayout); + } else { + m_paneStack->setLayoutStyle(PaneStack::SinglePropertyStackLayout); + } + } + } else if (name == "Background Mode" && m_viewManager) { + Preferences::BackgroundMode mode = + Preferences::getInstance()->getBackgroundMode(); + if (mode == Preferences::BackgroundFromTheme) { + m_viewManager->setGlobalDarkBackground(m_initialDarkBackground); + } else if (mode == Preferences::DarkBackground) { + m_viewManager->setGlobalDarkBackground(true); + } else { + m_viewManager->setGlobalDarkBackground(false); + } + if (m_viewManager->getGlobalDarkBackground()) { + m_panLayer->setBaseColour + (ColourDatabase::getInstance()->getColourIndex(tr("Bright Green"))); + } else { + m_panLayer->setBaseColour + (ColourDatabase::getInstance()->getColourIndex(tr("Green"))); + } + } +} + +void +MainWindow::play() +{ + if (m_playSource->isPlaying()) { + stop(); + } else { + playbackFrameChanged(m_viewManager->getPlaybackFrame()); + m_playSource->play(m_viewManager->getPlaybackFrame()); + } +} + +void +MainWindow::ffwd() +{ + if (!getMainModel()) return; + + int frame = m_viewManager->getPlaybackFrame(); + ++frame; + + Layer *layer = getSnapLayer(); + size_t sr = getMainModel()->getSampleRate(); + + if (!layer) { + + frame = RealTime::realTime2Frame + (RealTime::frame2RealTime(frame, sr) + RealTime(2, 0), sr); + if (frame > int(getMainModel()->getEndFrame())) { + frame = getMainModel()->getEndFrame(); + } + + } else { + + size_t resolution = 0; + if (!layer->snapToFeatureFrame(m_paneStack->getCurrentPane(), + frame, resolution, Layer::SnapRight)) { + frame = getMainModel()->getEndFrame(); + } + } + + if (frame < 0) frame = 0; + + if (m_viewManager->getPlaySelectionMode()) { + frame = m_viewManager->constrainFrameToSelection(size_t(frame)); + } + + m_viewManager->setPlaybackFrame(frame); +} + +void +MainWindow::ffwdEnd() +{ + if (!getMainModel()) return; + + size_t frame = getMainModel()->getEndFrame(); + + if (m_viewManager->getPlaySelectionMode()) { + frame = m_viewManager->constrainFrameToSelection(frame); + } + + m_viewManager->setPlaybackFrame(frame); +} + +void +MainWindow::rewind() +{ + if (!getMainModel()) return; + + int frame = m_viewManager->getPlaybackFrame(); + if (frame > 0) --frame; + + Layer *layer = getSnapLayer(); + size_t sr = getMainModel()->getSampleRate(); + + // when rewinding during playback, we want to allow a period + // following a rewind target point at which the rewind will go to + // the prior point instead of the immediately neighbouring one + if (m_playSource && m_playSource->isPlaying()) { + RealTime ct = RealTime::frame2RealTime(frame, sr); + ct = ct - RealTime::fromSeconds(0.25); + if (ct < RealTime::zeroTime) ct = RealTime::zeroTime; +// std::cerr << "rewind: frame " << frame << " -> "; + frame = RealTime::realTime2Frame(ct, sr); +// std::cerr << frame << std::endl; + } + + if (!layer) { + + frame = RealTime::realTime2Frame + (RealTime::frame2RealTime(frame, sr) - RealTime(2, 0), sr); + if (frame < int(getMainModel()->getStartFrame())) { + frame = getMainModel()->getStartFrame(); + } + + } else { + + size_t resolution = 0; + if (!layer->snapToFeatureFrame(m_paneStack->getCurrentPane(), + frame, resolution, Layer::SnapLeft)) { + frame = getMainModel()->getStartFrame(); + } + } + + if (frame < 0) frame = 0; + + if (m_viewManager->getPlaySelectionMode()) { + frame = m_viewManager->constrainFrameToSelection(size_t(frame)); + } + + m_viewManager->setPlaybackFrame(frame); +} + +void +MainWindow::rewindStart() +{ + if (!getMainModel()) return; + + size_t frame = getMainModel()->getStartFrame(); + + if (m_viewManager->getPlaySelectionMode()) { + frame = m_viewManager->constrainFrameToSelection(frame); + } + + m_viewManager->setPlaybackFrame(frame); +} + +Layer * +MainWindow::getSnapLayer() const +{ + Pane *pane = m_paneStack->getCurrentPane(); + if (!pane) return 0; + + Layer *layer = pane->getSelectedLayer(); + + if (!dynamic_cast(layer) && + !dynamic_cast(layer) && + !dynamic_cast(layer)) { + + layer = 0; + + for (int i = pane->getLayerCount(); i > 0; --i) { + Layer *l = pane->getLayer(i-1); + if (dynamic_cast(l)) { + layer = l; + break; + } + } + } + + return layer; +} + +void +MainWindow::stop() +{ + m_playSource->stop(); + + if (m_paneStack && m_paneStack->getCurrentPane()) { + updateVisibleRangeDisplay(m_paneStack->getCurrentPane()); + } else { + m_myStatusMessage = ""; + statusBar()->showMessage(""); + } +} + +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; + } + + addPane(i->second, action->text()); +} + +void +MainWindow::addPane(const PaneConfiguration &configuration, QString text) +{ + CommandHistory::getInstance()->startCompoundOperation(text, true); + + AddPaneCommand *command = new AddPaneCommand(this); + CommandHistory::getInstance()->addCommand(command); + + Pane *pane = command->getPane(); + + if (configuration.layer == LayerFactory::Spectrum) { + pane->setPlaybackFollow(PlaybackScrollContinuous); + pane->setFollowGlobalZoom(false); + pane->setZoomLevel(512); + } + + if (configuration.layer != LayerFactory::TimeRuler && + configuration.layer != LayerFactory::Spectrum) { + + 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(configuration.layer); + + Model *suggestedModel = configuration.sourceModel; + Model *model = 0; + + if (suggestedModel) { + + // check its validity + std::vector inputModels = m_document->getTransformInputModels(); + for (size_t j = 0; j < inputModels.size(); ++j) { + if (inputModels[j] == suggestedModel) { + model = suggestedModel; + break; + } + } + + if (!model) { + std::cerr << "WARNING: Model " << (void *)suggestedModel + << " appears in pane action map, but is not reported " + << "by document as a valid transform source" << std::endl; + } + } + + if (!model) model = m_document->getMainModel(); + + m_document->setModel(newLayer, model); + + m_document->setChannel(newLayer, configuration.channel); + m_document->addLayerToView(pane, newLayer); + + m_paneStack->setCurrentPane(pane); + m_paneStack->setCurrentLayer(pane, newLayer); + +// std::cerr << "MainWindow::addPane: global centre frame is " +// << m_viewManager->getGlobalCentreFrame() << std::endl; +// pane->setCentreFrame(m_viewManager->getGlobalCentreFrame()); + + 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(); + + connect(m_pane, SIGNAL(contextHelpChanged(const QString &)), + m_mw, SLOT(contextHelpChanged(const QString &))); + } else { + m_mw->m_paneStack->showPane(m_pane); + } + + m_mw->m_paneStack->setCurrentPane(m_pane); + m_mw->m_overview->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_overview->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_overview->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_overview->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; + } + + ei = m_sliceActions.find(action); + + if (ei != m_sliceActions.end()) { + Layer *newLayer = m_document->createLayer(LayerFactory::Slice); +// document->setModel(newLayer, ei->second->getModel()); + SliceableLayer *source = dynamic_cast(ei->second); + SliceLayer *dest = dynamic_cast(newLayer); + if (source && dest) { + dest->setSliceableModel(source->getSliceableModel()); + connect(source, SIGNAL(sliceableModelReplaced(const Model *, const Model *)), + dest, SLOT(sliceableModelReplaced(const Model *, const Model *))); + connect(m_document, SIGNAL(modelAboutToBeDeleted(Model *)), + dest, SLOT(modelAboutToBeDeleted(Model *))); + } + m_document->addLayerToView(pane, newLayer); + m_paneStack->setCurrentLayer(pane, newLayer); + return; + } + + TransformActionMap::iterator i = m_transformActions.find(action); + + if (i == m_transformActions.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; + } + + TransformId 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; + } + } + + // We always ask for configuration, even if the plugin isn't + // supposed to be configurable, because we need to let the user + // change the execution context (block size etc). + + PluginTransform::ExecutionContext context(channel); + + std::vector candidateInputModels = + m_document->getTransformInputModels(); + + Model *inputModel = factory->getConfigurationForTransform(transform, + candidateInputModels, + context, + configurationXml, + m_playSource); + if (!inputModel) return; + +// std::cerr << "MainWindow::addLayer: Input model is " << inputModel << " \"" << inputModel->objectName().toStdString() << "\"" << std::endl; + + Layer *newLayer = m_document->createDerivedLayer(transform, + inputModel, + context, + configurationXml); + + if (newLayer) { + m_document->addLayerToView(pane, newLayer); + m_document->setChannel(newLayer, context.channel); + m_recentTransforms.add(transform); + m_paneStack->setCurrentLayer(pane, newLayer); + } + + 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); + setupExistingLayersMenus(); + } + } + } +} + +void +MainWindow::playSpeedChanged(int position) +{ + PlaySpeedRangeMapper mapper(0, 200); + + float percent = m_playSpeed->mappedValue(); + float factor = mapper.getFactorForValue(percent); + + std::cerr << "speed = " << position << " percent = " << percent << " factor = " << factor << std::endl; + + bool something = (position != 100); + + int pc = lrintf(percent); + + if (!something) { + contextHelpChanged(tr("Playback speed: Normal")); + } else { + contextHelpChanged(tr("Playback speed: %1%2%") + .arg(position > 100 ? "+" : "") + .arg(pc)); + } + + m_playSharpen->setEnabled(something); + m_playMono->setEnabled(something); + bool sharpen = (something && m_playSharpen->isChecked()); + bool mono = (something && m_playMono->isChecked()); + m_playSource->setTimeStretch(factor, sharpen, mono); + + updateMenuStates(); +} + +void +MainWindow::playSharpenToggled() +{ + QSettings settings; + settings.beginGroup("MainWindow"); + settings.setValue("playsharpen", m_playSharpen->isChecked()); + settings.endGroup(); + + playSpeedChanged(m_playSpeed->value()); +} + +void +MainWindow::playMonoToggled() +{ + QSettings settings; + settings.beginGroup("MainWindow"); + settings.setValue("playmono", m_playMono->isChecked()); + settings.endGroup(); + + playSpeedChanged(m_playSpeed->value()); +} + +void +MainWindow::speedUpPlayback() +{ + int value = m_playSpeed->value(); + value = value + m_playSpeed->pageStep(); + if (value > m_playSpeed->maximum()) value = m_playSpeed->maximum(); + m_playSpeed->setValue(value); +} + +void +MainWindow::slowDownPlayback() +{ + int value = m_playSpeed->value(); + value = value - m_playSpeed->pageStep(); + if (value < m_playSpeed->minimum()) value = m_playSpeed->minimum(); + m_playSpeed->setValue(value); +} + +void +MainWindow::restoreNormalPlayback() +{ + m_playSpeed->setValue(m_playSpeed->defaultValue()); +} + +void +MainWindow::playbackFrameChanged(unsigned long frame) +{ + if (!(m_playSource && m_playSource->isPlaying()) || !getMainModel()) return; + + RealTime now = RealTime::frame2RealTime + (frame, getMainModel()->getSampleRate()); + + if (now.sec == m_lastPlayStatusSec) return; + + RealTime then = RealTime::frame2RealTime + (m_playSource->getPlayEndFrame(), getMainModel()->getSampleRate()); + + QString nowStr; + QString thenStr; + QString remainingStr; + + if (then.sec > 10) { + nowStr = now.toSecText().c_str(); + thenStr = then.toSecText().c_str(); + remainingStr = (then - now).toSecText().c_str(); + m_lastPlayStatusSec = now.sec; + } else { + nowStr = now.toText(true).c_str(); + thenStr = then.toText(true).c_str(); + remainingStr = (then - now).toText(true).c_str(); + } + + m_myStatusMessage = tr("Playing: %1 of %2 (%3 remaining)") + .arg(nowStr).arg(thenStr).arg(remainingStr); + + statusBar()->showMessage(m_myStatusMessage); +} + +void +MainWindow::globalCentreFrameChanged(unsigned long ) +{ + if ((m_playSource && m_playSource->isPlaying()) || !getMainModel()) return; + Pane *p = 0; + if (!m_paneStack || !(p = m_paneStack->getCurrentPane())) return; + if (!p->getFollowGlobalPan()) return; + updateVisibleRangeDisplay(p); +} + +void +MainWindow::viewCentreFrameChanged(View *v, unsigned long ) +{ + if ((m_playSource && m_playSource->isPlaying()) || !getMainModel()) return; + Pane *p = 0; + if (!m_paneStack || !(p = m_paneStack->getCurrentPane())) return; + if (v == p) updateVisibleRangeDisplay(p); +} + +void +MainWindow::viewZoomLevelChanged(View *v, unsigned long , bool ) +{ + if ((m_playSource && m_playSource->isPlaying()) || !getMainModel()) return; + Pane *p = 0; + if (!m_paneStack || !(p = m_paneStack->getCurrentPane())) return; + if (v == p) updateVisibleRangeDisplay(p); +} + +void +MainWindow::updateVisibleRangeDisplay(Pane *p) const +{ + if (!getMainModel() || !p) { + return; + } + + bool haveSelection = false; + size_t startFrame = 0, endFrame = 0; + + if (m_viewManager && m_viewManager->haveInProgressSelection()) { + + bool exclusive = false; + Selection s = m_viewManager->getInProgressSelection(exclusive); + + if (!s.isEmpty()) { + haveSelection = true; + startFrame = s.getStartFrame(); + endFrame = s.getEndFrame(); + } + } + + if (!haveSelection) { + startFrame = p->getFirstVisibleFrame(); + endFrame = p->getLastVisibleFrame(); + } + + RealTime start = RealTime::frame2RealTime + (startFrame, getMainModel()->getSampleRate()); + + RealTime end = RealTime::frame2RealTime + (endFrame, getMainModel()->getSampleRate()); + + RealTime duration = end - start; + + QString startStr, endStr, durationStr; + startStr = start.toText(true).c_str(); + endStr = end.toText(true).c_str(); + durationStr = duration.toText(true).c_str(); + + if (haveSelection) { + m_myStatusMessage = tr("Selection: %1 to %2 (duration %3)") + .arg(startStr).arg(endStr).arg(durationStr); + } else { + m_myStatusMessage = tr("Visible: %1 to %2 (duration %3)") + .arg(startStr).arg(endStr).arg(durationStr); + } + + statusBar()->showMessage(m_myStatusMessage); +} + +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 and pitch.") + .arg(requested).arg(actual)); + } + + updateDescriptionLabel(); +} + +void +MainWindow::audioOverloadPluginDisabled() +{ + QMessageBox::information + (this, tr("Audio processing overload"), + tr("Audio effects plugin auditioning has been disabled\ndue to a processing overload.")); +} + +void +MainWindow::layerAdded(Layer *) +{ +// std::cerr << "MainWindow::layerAdded(" << layer << ")" << std::endl; +// setupExistingLayersMenu(); + updateMenuStates(); +} + +void +MainWindow::layerRemoved(Layer *) +{ +// std::cerr << "MainWindow::layerRemoved(" << layer << ")" << std::endl; + setupExistingLayersMenus(); + 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); + } + } + + setupExistingLayersMenus(); + updateMenuStates(); +} + +void +MainWindow::modelAdded(Model *model) +{ +// std::cerr << "MainWindow::modelAdded(" << model << ")" << std::endl; + m_playSource->addModel(model); + if (dynamic_cast(model)) { + setupPaneAndLayerMenus(); + } +} + +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 && m_audioOutput) createPlayTarget(); +} + +void +MainWindow::modelAboutToBeDeleted(Model *model) +{ +// std::cerr << "MainWindow::modelAboutToBeDeleted(" << model << ")" << std::endl; + if (model == m_viewManager->getPlaybackModel()) { + m_viewManager->setPlaybackModel(0); + } + m_playSource->removeModel(model); + FFTDataServer::modelAboutToBeDeleted(model); +} + +void +MainWindow::modelGenerationFailed(QString transformName) +{ + QMessageBox::warning + (this, + tr("Failed to generate layer"), + tr("Failed to generate a derived layer.\n\nThe layer transform \"%1\" failed.\n\nThis probably means that a plugin failed to initialise, perhaps because it\nrejected the processing block size that was requested.") + .arg(transformName), + QMessageBox::Ok); +} + +void +MainWindow::modelRegenerationFailed(QString layerName, QString transformName) +{ + QMessageBox::warning + (this, + tr("Failed to regenerate layer"), + tr("Failed to regenerate derived layer \"%1\".\n\nThe layer transform \"%2\" failed to run.\n\nThis probably means the layer used a plugin that is not currently available.") + .arg(layerName).arg(transformName), + QMessageBox::Ok); +} + +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::propertyStacksResized() +{ +/* + std::cerr << "MainWindow::propertyStacksResized" << std::endl; + Pane *pane = m_paneStack->getCurrentPane(); + if (pane && m_overview) { + std::cerr << "Fixed width: " << pane->width() << std::endl; + m_overview->setFixedWidth(pane->width()); + } +*/ +} + +void +MainWindow::showLayerTree() +{ + if (!m_layerTreeView.isNull()) { + m_layerTreeView->show(); + m_layerTreeView->raise(); + return; + } + + //!!! should use an actual dialog class + + m_layerTreeView = new QTreeView(); + LayerTreeModel *tree = new LayerTreeModel(m_paneStack); + m_layerTreeView->resize(500, 300); //!!! + m_layerTreeView->setModel(tree); + m_layerTreeView->expandAll(); + m_layerTreeView->show(); +} + +void +MainWindow::pollOSC() +{ + if (!m_oscQueue || m_oscQueue->isEmpty()) return; + std::cerr << "MainWindow::pollOSC: have " << m_oscQueue->getMessagesAvailable() << " messages" << std::endl; + + if (m_openingAudioFile) return; + + OSCMessage message = m_oscQueue->readMessage(); + + if (message.getTarget() != 0) { + return; //!!! for now -- this class is target 0, others not handled yet + } + + handleOSCMessage(message); +} + +void +MainWindow::handleOSCMessage(const OSCMessage &message) +{ + std::cerr << "MainWindow::handleOSCMessage: thread id = " + << QThread::currentThreadId() << std::endl; + + // This large function should really be abstracted out. + + if (message.getMethod() == "open") { + + if (message.getArgCount() == 1 && + message.getArg(0).canConvert(QVariant::String)) { + QString path = message.getArg(0).toString(); + if (openSomeFile(path, ReplaceMainModel) != FileOpenSucceeded) { + std::cerr << "MainWindow::handleOSCMessage: File open failed for path \"" + << path.toStdString() << "\"" << std::endl; + } + //!!! we really need to spin here and not return until the + // file has been completely decoded... + } + + } else if (message.getMethod() == "openadditional") { + + if (message.getArgCount() == 1 && + message.getArg(0).canConvert(QVariant::String)) { + QString path = message.getArg(0).toString(); + if (openSomeFile(path, CreateAdditionalModel) != FileOpenSucceeded) { + std::cerr << "MainWindow::handleOSCMessage: File open failed for path \"" + << path.toStdString() << "\"" << std::endl; + } + } + + } else if (message.getMethod() == "recent" || + message.getMethod() == "last") { + + int n = 0; + if (message.getMethod() == "recent" && + message.getArgCount() == 1 && + message.getArg(0).canConvert(QVariant::Int)) { + n = message.getArg(0).toInt() - 1; + } + std::vector recent = m_recentFiles.getRecent(); + if (n >= 0 && n < int(recent.size())) { + if (openSomeFile(recent[n], ReplaceMainModel) != FileOpenSucceeded) { + std::cerr << "MainWindow::handleOSCMessage: File open failed for path \"" + << recent[n].toStdString() << "\"" << std::endl; + } + } + + } else if (message.getMethod() == "save") { + + QString path; + if (message.getArgCount() == 1 && + message.getArg(0).canConvert(QVariant::String)) { + path = message.getArg(0).toString(); + if (QFileInfo(path).exists()) { + std::cerr << "MainWindow::handleOSCMessage: Refusing to overwrite existing file in save" << std::endl; + } else { + saveSessionFile(path); + } + } + + } else if (message.getMethod() == "export") { + + QString path; + if (getMainModel()) { + if (message.getArgCount() == 1 && + message.getArg(0).canConvert(QVariant::String)) { + path = message.getArg(0).toString(); + if (QFileInfo(path).exists()) { + std::cerr << "MainWindow::handleOSCMessage: Refusing to overwrite existing file in export" << std::endl; + } else { + WavFileWriter writer(path, + getMainModel()->getSampleRate(), + getMainModel()->getChannelCount()); + MultiSelection ms = m_viewManager->getSelection(); + if (!ms.getSelections().empty()) { + writer.writeModel(getMainModel(), &ms); + } else { + writer.writeModel(getMainModel()); + } + } + } + } + + } else if (message.getMethod() == "jump" || + message.getMethod() == "play") { + + if (getMainModel()) { + + unsigned long frame = m_viewManager->getPlaybackFrame(); + bool selection = false; + bool play = (message.getMethod() == "play"); + + if (message.getArgCount() == 1) { + + if (message.getArg(0).canConvert(QVariant::String) && + message.getArg(0).toString() == "selection") { + + selection = true; + + } else if (message.getArg(0).canConvert(QVariant::String) && + message.getArg(0).toString() == "end") { + + frame = getMainModel()->getEndFrame(); + + } else if (message.getArg(0).canConvert(QVariant::Double)) { + + double time = message.getArg(0).toDouble(); + if (time < 0.0) time = 0.0; + + frame = lrint(time * getMainModel()->getSampleRate()); + } + } + + if (frame > getMainModel()->getEndFrame()) { + frame = getMainModel()->getEndFrame(); + } + + if (play) { + m_viewManager->setPlaySelectionMode(selection); + } + + if (selection) { + MultiSelection::SelectionList sl = m_viewManager->getSelections(); + if (!sl.empty()) { + frame = sl.begin()->getStartFrame(); + } + } + + m_viewManager->setPlaybackFrame(frame); + + if (play && !m_playSource->isPlaying()) { + m_playSource->play(frame); + } + } + + } else if (message.getMethod() == "stop") { + + if (m_playSource->isPlaying()) m_playSource->stop(); + + } else if (message.getMethod() == "loop") { + + if (message.getArgCount() == 1 && + message.getArg(0).canConvert(QVariant::String)) { + + QString str = message.getArg(0).toString(); + if (str == "on") { + m_viewManager->setPlayLoopMode(true); + } else if (str == "off") { + m_viewManager->setPlayLoopMode(false); + } + } + + } else if (message.getMethod() == "solo") { + + if (message.getArgCount() == 1 && + message.getArg(0).canConvert(QVariant::String)) { + + QString str = message.getArg(0).toString(); + if (str == "on") { + m_viewManager->setPlaySoloMode(true); + } else if (str == "off") { + m_viewManager->setPlaySoloMode(false); + } + } + + } else if (message.getMethod() == "select" || + message.getMethod() == "addselect") { + + if (getMainModel()) { + + int f0 = getMainModel()->getStartFrame(); + int f1 = getMainModel()->getEndFrame(); + + bool done = false; + + if (message.getArgCount() == 2 && + message.getArg(0).canConvert(QVariant::Double) && + message.getArg(1).canConvert(QVariant::Double)) { + + double t0 = message.getArg(0).toDouble(); + double t1 = message.getArg(1).toDouble(); + if (t1 < t0) { double temp = t0; t0 = t1; t1 = temp; } + if (t0 < 0.0) t0 = 0.0; + if (t1 < 0.0) t1 = 0.0; + + f0 = lrint(t0 * getMainModel()->getSampleRate()); + f1 = lrint(t1 * getMainModel()->getSampleRate()); + + Pane *pane = m_paneStack->getCurrentPane(); + Layer *layer = 0; + if (pane) layer = pane->getSelectedLayer(); + if (layer) { + size_t resolution; + layer->snapToFeatureFrame(pane, f0, resolution, + Layer::SnapLeft); + layer->snapToFeatureFrame(pane, f1, resolution, + Layer::SnapRight); + } + + } else if (message.getArgCount() == 1 && + message.getArg(0).canConvert(QVariant::String)) { + + QString str = message.getArg(0).toString(); + if (str == "none") { + m_viewManager->clearSelections(); + done = true; + } + } + + if (!done) { + if (message.getMethod() == "select") { + m_viewManager->setSelection(Selection(f0, f1)); + } else { + m_viewManager->addSelection(Selection(f0, f1)); + } + } + } + + } else if (message.getMethod() == "add") { + + if (getMainModel()) { + + if (message.getArgCount() >= 1 && + message.getArg(0).canConvert(QVariant::String)) { + + int channel = -1; + if (message.getArgCount() == 2 && + message.getArg(0).canConvert(QVariant::Int)) { + channel = message.getArg(0).toInt(); + if (channel < -1 || + channel > int(getMainModel()->getChannelCount())) { + std::cerr << "WARNING: MainWindow::handleOSCMessage: channel " + << channel << " out of range" << std::endl; + channel = -1; + } + } + + QString str = message.getArg(0).toString(); + + LayerFactory::LayerType type = + LayerFactory::getInstance()->getLayerTypeForName(str); + + if (type == LayerFactory::UnknownLayer) { + std::cerr << "WARNING: MainWindow::handleOSCMessage: unknown layer " + << "type " << str.toStdString() << std::endl; + } else { + + PaneConfiguration configuration(type, + getMainModel(), + channel); + + addPane(configuration, + tr("Add %1 Pane") + .arg(LayerFactory::getInstance()-> + getLayerPresentationName(type))); + } + } + } + + } else if (message.getMethod() == "undo") { + + CommandHistory::getInstance()->undo(); + + } else if (message.getMethod() == "redo") { + + CommandHistory::getInstance()->redo(); + + } else if (message.getMethod() == "set") { + + if (message.getArgCount() == 2 && + message.getArg(0).canConvert(QVariant::String) && + message.getArg(1).canConvert(QVariant::Double)) { + + QString property = message.getArg(0).toString(); + float value = (float)message.getArg(1).toDouble(); + + if (property == "gain") { + if (value < 0.0) value = 0.0; + m_fader->setValue(value); + if (m_playTarget) m_playTarget->setOutputGain(value); + } else if (property == "speedup") { + m_playSpeed->setMappedValue(value); + } else if (property == "overlays") { + if (value < 0.5) { + m_viewManager->setOverlayMode(ViewManager::NoOverlays); + } else if (value < 1.5) { + m_viewManager->setOverlayMode(ViewManager::MinimalOverlays); + } else if (value < 2.5) { + m_viewManager->setOverlayMode(ViewManager::StandardOverlays); + } else { + m_viewManager->setOverlayMode(ViewManager::AllOverlays); + } + } else if (property == "zoomwheels") { + m_viewManager->setZoomWheelsEnabled(value > 0.5); + } else if (property == "propertyboxes") { + bool toggle = ((value < 0.5) != + (m_paneStack->getLayoutStyle() == PaneStack::NoPropertyStacks)); + if (toggle) togglePropertyBoxes(); + } + + } else { + PropertyContainer *container = 0; + Pane *pane = m_paneStack->getCurrentPane(); + if (pane && + message.getArgCount() == 3 && + message.getArg(0).canConvert(QVariant::String) && + message.getArg(1).canConvert(QVariant::String) && + message.getArg(2).canConvert(QVariant::String)) { + if (message.getArg(0).toString() == "pane") { + container = pane->getPropertyContainer(0); + } else if (message.getArg(0).toString() == "layer") { + container = pane->getSelectedLayer(); + } + } + if (container) { + QString nameString = message.getArg(1).toString(); + QString valueString = message.getArg(2).toString(); + container->setPropertyWithCommand(nameString, valueString); + } + } + + } else if (message.getMethod() == "setcurrent") { + + int paneIndex = -1, layerIndex = -1; + bool wantLayer = false; + + if (message.getArgCount() >= 1 && + message.getArg(0).canConvert(QVariant::Int)) { + + paneIndex = message.getArg(0).toInt() - 1; + + if (message.getArgCount() >= 2 && + message.getArg(1).canConvert(QVariant::Int)) { + wantLayer = true; + layerIndex = message.getArg(1).toInt() - 1; + } + } + + if (paneIndex >= 0 && paneIndex < m_paneStack->getPaneCount()) { + Pane *pane = m_paneStack->getPane(paneIndex); + m_paneStack->setCurrentPane(pane); + if (layerIndex >= 0 && layerIndex < pane->getLayerCount()) { + Layer *layer = pane->getLayer(layerIndex); + m_paneStack->setCurrentLayer(pane, layer); + } else if (wantLayer && layerIndex == -1) { + m_paneStack->setCurrentLayer(pane, 0); + } + } + + } else if (message.getMethod() == "delete") { + + if (message.getArgCount() == 1 && + message.getArg(0).canConvert(QVariant::String)) { + + QString target = message.getArg(0).toString(); + + if (target == "pane") { + + deleteCurrentPane(); + + } else if (target == "layer") { + + deleteCurrentLayer(); + + } else { + + std::cerr << "WARNING: MainWindow::handleOSCMessage: Unknown delete target " << target.toStdString() << std::endl; + } + } + + } else if (message.getMethod() == "zoom") { + + if (message.getArgCount() == 1) { + if (message.getArg(0).canConvert(QVariant::String) && + message.getArg(0).toString() == "in") { + zoomIn(); + } else if (message.getArg(0).canConvert(QVariant::String) && + message.getArg(0).toString() == "out") { + zoomOut(); + } else if (message.getArg(0).canConvert(QVariant::String) && + message.getArg(0).toString() == "default") { + zoomDefault(); + } else if (message.getArg(0).canConvert(QVariant::Double)) { + double level = message.getArg(0).toDouble(); + Pane *currentPane = m_paneStack->getCurrentPane(); + if (level < 1.0) level = 1.0; + if (currentPane) currentPane->setZoomLevel(lrint(level)); + } + } + + } else if (message.getMethod() == "zoomvertical") { + + Pane *pane = m_paneStack->getCurrentPane(); + Layer *layer = 0; + if (pane && pane->getLayerCount() > 0) { + layer = pane->getLayer(pane->getLayerCount() - 1); + } + int defaultStep = 0; + int steps = 0; + if (layer) { + steps = layer->getVerticalZoomSteps(defaultStep); + if (message.getArgCount() == 1 && steps > 0) { + if (message.getArg(0).canConvert(QVariant::String) && + message.getArg(0).toString() == "in") { + int step = layer->getCurrentVerticalZoomStep() + 1; + if (step < steps) layer->setVerticalZoomStep(step); + } else if (message.getArg(0).canConvert(QVariant::String) && + message.getArg(0).toString() == "out") { + int step = layer->getCurrentVerticalZoomStep() - 1; + if (step >= 0) layer->setVerticalZoomStep(step); + } else if (message.getArg(0).canConvert(QVariant::String) && + message.getArg(0).toString() == "default") { + layer->setVerticalZoomStep(defaultStep); + } + } else if (message.getArgCount() == 2) { + if (message.getArg(0).canConvert(QVariant::Double) && + message.getArg(1).canConvert(QVariant::Double)) { + double min = message.getArg(0).toDouble(); + double max = message.getArg(1).toDouble(); + layer->setDisplayExtents(min, max); + } + } + } + + } else if (message.getMethod() == "quit") { + + m_abandoning = true; + close(); + + } else if (message.getMethod() == "resize") { + + if (message.getArgCount() == 2) { + + int width = 0, height = 0; + + if (message.getArg(1).canConvert(QVariant::Int)) { + + height = message.getArg(1).toInt(); + + if (message.getArg(0).canConvert(QVariant::String) && + message.getArg(0).toString() == "pane") { + + Pane *pane = m_paneStack->getCurrentPane(); + if (pane) pane->resize(pane->width(), height); + + } else if (message.getArg(0).canConvert(QVariant::Int)) { + + width = message.getArg(0).toInt(); + resize(width, height); + } + } + } + + } else if (message.getMethod() == "transform") { + + Pane *pane = m_paneStack->getCurrentPane(); + + if (getMainModel() && + pane && + message.getArgCount() == 1 && + message.getArg(0).canConvert(QVariant::String)) { + + TransformId transform = message.getArg(0).toString(); + + Layer *newLayer = m_document->createDerivedLayer + (transform, + getMainModel(), + TransformFactory::getInstance()->getDefaultContextForTransform + (transform, getMainModel()), + ""); + + if (newLayer) { + m_document->addLayerToView(pane, newLayer); + m_recentTransforms.add(transform); + m_paneStack->setCurrentLayer(pane, newLayer); + } + } + + } else { + std::cerr << "WARNING: MainWindow::handleOSCMessage: Unknown or unsupported " + << "method \"" << message.getMethod().toStdString() + << "\"" << std::endl; + } + +} + +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::mouseEnteredWidget() +{ + QWidget *w = dynamic_cast(sender()); + if (!w) return; + + if (w == m_fader) { + contextHelpChanged(tr("Adjust the master playback level")); + } else if (w == m_playSpeed) { + contextHelpChanged(tr("Adjust the master playback speed")); + } else if (w == m_playSharpen && w->isEnabled()) { + contextHelpChanged(tr("Toggle transient sharpening for playback time scaling")); + } else if (w == m_playMono && w->isEnabled()) { + contextHelpChanged(tr("Toggle mono mode for playback time scaling")); + } +} + +void +MainWindow::mouseLeftWidget() +{ + contextHelpChanged(""); +} + +void +MainWindow::inProgressSelectionChanged() +{ + Pane *currentPane = 0; + if (m_paneStack) currentPane = m_paneStack->getCurrentPane(); + if (currentPane) updateVisibleRangeDisplay(currentPane); +} + +void +MainWindow::contextHelpChanged(const QString &s) +{ + if (s == "" && m_myStatusMessage != "") { + statusBar()->showMessage(m_myStatusMessage); + return; + } + statusBar()->showMessage(s); +} + +void +MainWindow::website() +{ + openHelpUrl(tr("http://www.sonicvisualiser.org/")); +} + +void +MainWindow::help() +{ + openHelpUrl(tr("http://www.sonicvisualiser.org/doc/reference/1.0/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 VECT_VERSION +#ifdef SVNREV + version = tr("Release %1 : Revision %2").arg(VECT_VERSION).arg(SVNREV); +#else + version = tr("Release %1").arg(VECT_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 configuration

") + .arg(version) + .arg(debug ? tr("Debug") : tr("Release")); + +#ifndef BUILD_STATIC + aboutText += tr("
Using Qt v%1 © Trolltech AS").arg(QT_VERSION_STR); +#else +#ifdef QT_SHARED + aboutText += tr("
Using Qt v%1 © Trolltech AS").arg(QT_VERSION_STR); +#endif +#endif + +#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 +#ifdef JACK_VERSION + aboutText += tr("
With JACK audio output (v%1) © Paul Davis and Jack O'Quin").arg(JACK_VERSION); +#else + aboutText += tr("
With JACK audio output © Paul Davis and Jack O'Quin"); +#endif +#endif +#ifdef HAVE_PORTAUDIO + aboutText += tr("
With PortAudio audio output © Ross Bencina and Phil Burk"); +#endif +#ifdef HAVE_OGGZ +#ifdef OGGZ_VERSION + aboutText += tr("
With Ogg file decoder (oggz v%1, fishsound v%2) © CSIRO Australia").arg(OGGZ_VERSION).arg(FISHSOUND_VERSION); +#else + aboutText += tr("
With Ogg file decoder © CSIRO Australia"); +#endif +#endif +#ifdef HAVE_MAD +#ifdef MAD_VERSION + aboutText += tr("
With MAD mp3 decoder (v%1) © Underbit Technologies Inc").arg(MAD_VERSION); +#else + aboutText += tr("
With MAD mp3 decoder © Underbit Technologies Inc"); +#endif +#endif +#ifdef HAVE_SAMPLERATE +#ifdef SAMPLERATE_VERSION + aboutText += tr("
With libsamplerate (v%1) © Erik de Castro Lopo").arg(SAMPLERATE_VERSION); +#else + aboutText += tr("
With libsamplerate © Erik de Castro Lopo"); +#endif +#endif +#ifdef HAVE_SNDFILE +#ifdef SNDFILE_VERSION + aboutText += tr("
With libsndfile (v%1) © Erik de Castro Lopo").arg(SNDFILE_VERSION); +#else + aboutText += tr("
With libsndfile © Erik de Castro Lopo"); +#endif +#endif +#ifdef HAVE_FFTW3F +#ifdef FFTW3_VERSION + aboutText += tr("
With FFTW3 (v%1) © Matteo Frigo and MIT").arg(FFTW3_VERSION); +#else + aboutText += tr("
With FFTW3 © Matteo Frigo and MIT"); +#endif +#endif +#ifdef HAVE_VAMP + aboutText += tr("
With Vamp plugin support (API v%1, host 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); +#ifdef HAVE_LIBLO +#ifdef LIBLO_VERSION + aboutText += tr("
With liblo Lite OSC library (v%1) © Steve Harris").arg(LIBLO_VERSION); +#else + aboutText += tr("
With liblo Lite OSC library © Steve Harris").arg(LIBLO_VERSION); +#endif + if (m_oscQueue && m_oscQueue->isOK()) { + aboutText += tr("

The OSC URL for this instance is: \"%1\"").arg(m_oscQueue->getOSCURL()); + } +#endif + aboutText += "

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

Sonic Visualiser Copyright © 2005 - 2007 Chris Cannam and
" + "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); +} + +void +MainWindow::keyReference() +{ + m_keyReference->show(); +} + diff --git a/main/MainWindow.h b/main/MainWindow.h new file mode 100644 index 0000000..2133e12 --- /dev/null +++ b/main/MainWindow.h @@ -0,0 +1,438 @@ +/* -*- 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 and QMUL. + + 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 + +#include "base/Command.h" +#include "view/ViewManager.h" +#include "base/PropertyContainer.h" +#include "base/RecentFiles.h" +#include "layer/LayerFactory.h" +#include "transform/Transform.h" +#include "document/SVFileReader.h" +#include "data/fileio/FileFinder.h" +#include + +class Document; +class PaneStack; +class Pane; +class View; +class Fader; +class Overview; +class Layer; +class WaveformLayer; +class WaveFileModel; +class AudioCallbackPlaySource; +class AudioCallbackPlayTarget; +class CommandHistory; +class QMenu; +class AudioDial; +class QLabel; +class QCheckBox; +class PreferencesDialog; +class QTreeView; +class QPushButton; +class OSCQueue; +class OSCMessage; +class KeyReference; + + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(bool withAudioOutput = true, + bool withOSCSupport = true); + virtual ~MainWindow(); + + enum AudioFileOpenMode { + ReplaceMainModel, + CreateAdditionalModel, + AskUser + }; + + enum FileOpenStatus { + FileOpenSucceeded, + FileOpenFailed, + FileOpenCancelled + }; + + FileOpenStatus openSomeFile(QString path, AudioFileOpenMode = AskUser); + FileOpenStatus openAudioFile(QString path, AudioFileOpenMode = AskUser); + FileOpenStatus openPlaylistFile(QString path, AudioFileOpenMode = AskUser); + FileOpenStatus openLayerFile(QString path); + FileOpenStatus openSessionFile(QString path); + FileOpenStatus openURL(QUrl url, AudioFileOpenMode = AskUser); + FileOpenStatus openURL(QString url, AudioFileOpenMode = AskUser); + + bool saveSessionFile(QString path); + bool commitData(bool mayAskUser); // on session shutdown + +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 canExportImage(bool); + void canRenameLayer(bool); + void canEditLayer(bool); + void canMeasureLayer(bool); + void canSelect(bool); + void canClearSelection(bool); + void canEditSelection(bool); + void canDeleteSelection(bool); + void canPaste(bool); + void canInsertInstant(bool); + void canInsertInstantsAtBoundaries(bool); + void canDeleteCurrentLayer(bool); + void canZoom(bool); + void canScroll(bool); + void canPlay(bool); + void canFfwd(bool); + void canRewind(bool); + void canPlaySelection(bool); + void canSpeedUpPlayback(bool); + void canSlowDownPlayback(bool); + void canChangePlaybackSpeed(bool); + void canSave(bool); + +public slots: + void preferenceChanged(PropertyContainer::PropertyName); + +protected slots: + void openSession(); + void importAudio(); + void importMoreAudio(); + void openSomething(); + void openLocation(); + void openRecentFile(); + void exportAudio(); + void importLayer(); + void exportLayer(); + void exportImage(); + 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 showMinimalOverlays(); + void showStandardOverlays(); + void showAllOverlays(); + + void toggleZoomWheels(); + void togglePropertyBoxes(); + void toggleStatusBar(); + + 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 playSoloToggled(); + void playSpeedChanged(int); + void playSharpenToggled(); + void playMonoToggled(); + void speedUpPlayback(); + void slowDownPlayback(); + void restoreNormalPlayback(); + void sampleRateMismatch(size_t, size_t, bool); + void audioOverloadPluginDisabled(); + + void playbackFrameChanged(unsigned long); + void globalCentreFrameChanged(unsigned long); + void viewCentreFrameChanged(View *, unsigned long); + void viewZoomLevelChanged(View *, unsigned long, bool); + void outputLevelsChanged(float, float); + + void currentPaneChanged(Pane *); + void currentLayerChanged(Pane *, Layer *); + + void toolNavigateSelected(); + void toolSelectSelected(); + void toolEditSelected(); + void toolDrawSelected(); + void toolMeasureSelected(); + + void selectAll(); + void selectToStart(); + void selectToEnd(); + void selectVisible(); + void clearSelection(); + void cut(); + void copy(); + void paste(); + void deleteSelected(); + void insertInstant(); + void insertInstantAt(size_t); + void insertInstantsAtBoundaries(); + + 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 propertyStacksResized(); + + void setupRecentFilesMenu(); + void setupRecentTransformsMenu(); + + void showLayerTree(); + + void pollOSC(); + void handleOSCMessage(const OSCMessage &); + + void mouseEnteredWidget(); + void mouseLeftWidget(); + void contextHelpChanged(const QString &); + void inProgressSelectionChanged(); + + void website(); + void help(); + void about(); + void keyReference(); + +protected: + QString m_sessionFile; + QString m_audioFile; + Document *m_document; + + QLabel *m_descriptionLabel; + PaneStack *m_paneStack; + ViewManager *m_viewManager; + Overview *m_overview; + Fader *m_fader; + AudioDial *m_playSpeed; + QPushButton *m_playSharpen; + QPushButton *m_playMono; + WaveformLayer *m_panLayer; + Layer *m_timeRulerLayer; + + bool m_audioOutput; + AudioCallbackPlaySource *m_playSource; + AudioCallbackPlayTarget *m_playTarget; + + OSCQueue *m_oscQueue; + + RecentFiles m_recentFiles; + RecentFiles m_recentTransforms; + + bool m_mainMenusCreated; + QMenu *m_paneMenu; + QMenu *m_layerMenu; + QMenu *m_transformsMenu; + QMenu *m_playbackMenu; + QMenu *m_existingLayersMenu; + QMenu *m_sliceMenu; + QMenu *m_recentFilesMenu; + QMenu *m_recentTransformsMenu; + QMenu *m_rightButtonMenu; + QMenu *m_rightButtonLayerMenu; + QMenu *m_rightButtonTransformsMenu; + QMenu *m_rightButtonPlaybackMenu; + + QAction *m_deleteSelectedAction; + QAction *m_ffwdAction; + QAction *m_rwdAction; + + bool m_documentModified; + bool m_openingAudioFile; + bool m_abandoning; + + int m_lastPlayStatusSec; + mutable QString m_myStatusMessage; + + QPointer m_preferencesDialog; + QPointer m_layerTreeView; + + bool m_initialDarkBackground; + + KeyReference *m_keyReference; + + WaveFileModel *getMainModel(); + const WaveFileModel *getMainModel() const; + void createDocument(); + + struct PaneConfiguration { + PaneConfiguration(LayerFactory::LayerType _layer + = LayerFactory::TimeRuler, + Model *_source = 0, + int _channel = -1) : + layer(_layer), sourceModel(_source), channel(_channel) { } + LayerFactory::LayerType layer; + Model *sourceModel; + int channel; + }; + + typedef std::map PaneActionMap; + PaneActionMap m_paneActions; + + typedef std::map TransformActionMap; + TransformActionMap m_transformActions; + + typedef std::map TransformActionReverseMap; + TransformActionReverseMap m_transformActionsReverse; + + typedef std::map LayerActionMap; + LayerActionMap m_layerActions; + + typedef std::map ExistingLayerActionMap; + ExistingLayerActionMap m_existingLayerActions; + ExistingLayerActionMap m_sliceActions; + + typedef std::map ToolActionMap; + ToolActionMap m_toolActions; + + void setupMenus(); + void setupFileMenu(); + void setupEditMenu(); + void setupViewMenu(); + void setupPaneAndLayerMenus(); + void setupTransformsMenu(); + void setupHelpMenu(); + void setupExistingLayersMenus(); + void setupToolbars(); + + Pane *addPaneToStack(); + + void addPane(const PaneConfiguration &configuration, QString text); + + Layer *getSnapLayer() const; + + 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(); + + FileOpenStatus openSomeFile(QString path, QString location, + AudioFileOpenMode = AskUser); + FileOpenStatus openAudioFile(QString path, QString location, + AudioFileOpenMode = AskUser); + FileOpenStatus openPlaylistFile(QString path, QString location, + AudioFileOpenMode = AskUser); + FileOpenStatus openLayerFile(QString path, QString location); + FileOpenStatus openSessionFile(QString path, QString location); + + QString getOpenFileName(FileFinder::FileType type); + QString getSaveFileName(FileFinder::FileType type); + void registerLastOpenedFilePath(FileFinder::FileType type, QString path); + + void createPlayTarget(); + + void openHelpUrl(QString url); + + void updateVisibleRangeDisplay(Pane *p) const; + + void toXml(QTextStream &stream); +}; + + +#endif diff --git a/main/PreferencesDialog.cpp b/main/PreferencesDialog.cpp new file mode 100644 index 0000000..0a629d5 --- /dev/null +++ b/main/PreferencesDialog.cpp @@ -0,0 +1,379 @@ +/* -*- 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 +#include + +#include "widgets/WindowTypeSelector.h" +#include "widgets/IconLoader.h" +#include "base/Preferences.h" + +PreferencesDialog::PreferencesDialog(QWidget *parent, Qt::WFlags flags) : + QDialog(parent, flags), + m_changesOnRestart(false) +{ + setWindowTitle(tr("Sonic Visualiser: Application Preferences")); + + Preferences *prefs = Preferences::getInstance(); + + QGridLayout *grid = new QGridLayout; + setLayout(grid); + + QTabWidget *tab = new QTabWidget; + grid->addWidget(tab, 0, 0); + + tab->setTabPosition(QTabWidget::North); + + // Create this first, as slots that get called from the ctor will + // refer to it + m_applyButton = new QPushButton(tr("Apply")); + + // Create all the preference widgets first, then create the + // individual tab widgets and place the preferences in their + // appropriate places in one go afterwards + + int min, max, deflt, i; + + m_windowType = WindowType(prefs->getPropertyRangeAndValue + ("Window Type", &min, &max, &deflt)); + m_windowTypeSelector = new WindowTypeSelector(m_windowType); + + connect(m_windowTypeSelector, SIGNAL(windowTypeChanged(WindowType)), + this, SLOT(windowTypeChanged(WindowType))); + + QComboBox *smoothing = new QComboBox; + + int sm = prefs->getPropertyRangeAndValue("Spectrogram Smoothing", &min, &max, + &deflt); + m_spectrogramSmoothing = sm; + + for (i = min; i <= max; ++i) { + smoothing->addItem(prefs->getPropertyValueLabel("Spectrogram Smoothing", i)); + } + + smoothing->setCurrentIndex(sm); + + connect(smoothing, SIGNAL(currentIndexChanged(int)), + this, SLOT(spectrogramSmoothingChanged(int))); + + QComboBox *propertyLayout = new QComboBox; + int pl = prefs->getPropertyRangeAndValue("Property Box Layout", &min, &max, + &deflt); + 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))); + + QComboBox *resampleQuality = new QComboBox; + + int rsq = prefs->getPropertyRangeAndValue("Resample Quality", &min, &max, + &deflt); + m_resampleQuality = rsq; + + for (i = min; i <= max; ++i) { + resampleQuality->addItem(prefs->getPropertyValueLabel("Resample Quality", i)); + } + + resampleQuality->setCurrentIndex(rsq); + + connect(resampleQuality, SIGNAL(currentIndexChanged(int)), + this, SLOT(resampleQualityChanged(int))); + + QCheckBox *resampleOnLoad = new QCheckBox; + m_resampleOnLoad = prefs->getResampleOnLoad(); + resampleOnLoad->setCheckState(m_resampleOnLoad ? Qt::Checked : + Qt::Unchecked); + connect(resampleOnLoad, SIGNAL(stateChanged(int)), + this, SLOT(resampleOnLoadChanged(int))); + + m_tempDirRootEdit = new QLineEdit; + QString dir = prefs->getTemporaryDirectoryRoot(); + m_tempDirRoot = dir; + dir.replace("$HOME", tr("")); + m_tempDirRootEdit->setText(dir); + m_tempDirRootEdit->setReadOnly(true); + QPushButton *tempDirButton = new QPushButton; + tempDirButton->setIcon(IconLoader().load("fileopen")); + connect(tempDirButton, SIGNAL(clicked()), + this, SLOT(tempDirButtonClicked())); + tempDirButton->setFixedSize(QSize(24, 24)); + + QComboBox *bgMode = new QComboBox; + int bg = prefs->getPropertyRangeAndValue("Background Mode", &min, &max, + &deflt); + m_backgroundMode = bg; + for (i = min; i <= max; ++i) { + bgMode->addItem(prefs->getPropertyValueLabel("Background Mode", i)); + } + bgMode->setCurrentIndex(bg); + + connect(bgMode, SIGNAL(currentIndexChanged(int)), + this, SLOT(backgroundModeChanged(int))); + + // General tab + + QFrame *frame = new QFrame; + + QGridLayout *subgrid = new QGridLayout; + frame->setLayout(subgrid); + + 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 + ("Background Mode"))), + row, 0); + subgrid->addWidget(bgMode, row++, 1, 1, 2); + + subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel + ("Resample On Load"))), + row, 0); + subgrid->addWidget(resampleOnLoad, row++, 1, 1, 1); + + subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel + ("Resample Quality"))), + row, 0); + subgrid->addWidget(resampleQuality, row++, 1, 1, 2); + + subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel + ("Temporary Directory Root"))), + row, 0); + subgrid->addWidget(m_tempDirRootEdit, row, 1, 1, 1); + subgrid->addWidget(tempDirButton, row, 2, 1, 1); + row++; + + subgrid->setRowStretch(row, 10); + + tab->addTab(frame, tr("&General")); + + // Analysis tab + + frame = new QFrame; + subgrid = new QGridLayout; + frame->setLayout(subgrid); + row = 0; + + 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 + ("Spectrogram Smoothing")), + row, 0); + subgrid->addWidget(smoothing, row++, 1, 1, 2); + + subgrid->addWidget(new QLabel(tr("%1:").arg(prefs->getPropertyLabel + ("Window Type"))), + row, 0); + subgrid->addWidget(m_windowTypeSelector, row++, 1, 2, 2); + subgrid->setRowStretch(row, 10); + row++; + + subgrid->setRowStretch(row, 10); + + tab->addTab(frame, tr("&Analysis")); + + QDialogButtonBox *bb = new QDialogButtonBox(Qt::Horizontal); + grid->addWidget(bb, 1, 0); + + QPushButton *ok = new QPushButton(tr("OK")); + QPushButton *cancel = new QPushButton(tr("Cancel")); + bb->addButton(ok, QDialogButtonBox::AcceptRole); + bb->addButton(m_applyButton, QDialogButtonBox::ApplyRole); + bb->addButton(cancel, QDialogButtonBox::RejectRole); + 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; +} + +void +PreferencesDialog::windowTypeChanged(WindowType type) +{ + m_windowType = type; + m_applyButton->setEnabled(true); +} + +void +PreferencesDialog::spectrogramSmoothingChanged(int smoothing) +{ + m_spectrogramSmoothing = smoothing; + 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::resampleQualityChanged(int q) +{ + m_resampleQuality = q; + m_applyButton->setEnabled(true); +} + +void +PreferencesDialog::resampleOnLoadChanged(int state) +{ + m_resampleOnLoad = (state == Qt::Checked); + m_applyButton->setEnabled(true); + m_changesOnRestart = true; +} + +void +PreferencesDialog::tempDirRootChanged(QString r) +{ + m_tempDirRoot = r; + m_applyButton->setEnabled(true); +} + +void +PreferencesDialog::tempDirButtonClicked() +{ + QString dir = QFileDialog::getExistingDirectory + (this, tr("Select a directory to create cache subdirectory in"), + m_tempDirRoot); + if (dir == "") return; + m_tempDirRootEdit->setText(dir); + tempDirRootChanged(dir); + m_changesOnRestart = true; +} + +void +PreferencesDialog::backgroundModeChanged(int mode) +{ + m_backgroundMode = mode; + m_applyButton->setEnabled(true); + m_changesOnRestart = true; +} + +void +PreferencesDialog::okClicked() +{ + applyClicked(); + accept(); +} + +void +PreferencesDialog::applyClicked() +{ + Preferences *prefs = Preferences::getInstance(); + prefs->setWindowType(WindowType(m_windowType)); + prefs->setSpectrogramSmoothing(Preferences::SpectrogramSmoothing + (m_spectrogramSmoothing)); + prefs->setPropertyBoxLayout(Preferences::PropertyBoxLayout + (m_propertyLayout)); + prefs->setTuningFrequency(m_tuningFrequency); + prefs->setResampleQuality(m_resampleQuality); + prefs->setResampleOnLoad(m_resampleOnLoad); + prefs->setTemporaryDirectoryRoot(m_tempDirRoot); + prefs->setBackgroundMode(Preferences::BackgroundMode(m_backgroundMode)); + + m_applyButton->setEnabled(false); + + if (m_changesOnRestart) { + QMessageBox::information(this, tr("Preferences"), + tr("One or more of the application preferences you have changed may not take full effect until Sonic Visualiser is restarted.\nPlease exit and restart the application now if you want these changes to take effect immediately.")); + m_changesOnRestart = false; + } +} + +void +PreferencesDialog::cancelClicked() +{ + reject(); +} + +void +PreferencesDialog::applicationClosing(bool quickly) +{ + if (quickly) { + reject(); + return; + } + + if (m_applyButton->isEnabled()) { + int rv = QMessageBox::warning + (this, tr("Preferences Changed"), + tr("Some preferences have been changed but not applied.\n" + "Apply them before closing?"), + QMessageBox::Apply | QMessageBox::Discard, + QMessageBox::Discard); + if (rv == QMessageBox::Apply) { + applyClicked(); + accept(); + } else { + reject(); + } + } else { + accept(); + } +} + diff --git a/main/PreferencesDialog.h b/main/PreferencesDialog.h new file mode 100644 index 0000000..90e47dd --- /dev/null +++ b/main/PreferencesDialog.h @@ -0,0 +1,72 @@ +/* -*- 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 WindowTypeSelector; +class QPushButton; +class QLineEdit; + +class PreferencesDialog : public QDialog +{ + Q_OBJECT + +public: + PreferencesDialog(QWidget *parent = 0, Qt::WFlags flags = 0); + virtual ~PreferencesDialog(); + +public slots: + void applicationClosing(bool quickly); + +protected slots: + void windowTypeChanged(WindowType type); + void spectrogramSmoothingChanged(int state); + void propertyLayoutChanged(int layout); + void tuningFrequencyChanged(double freq); + void resampleQualityChanged(int quality); + void resampleOnLoadChanged(int state); + void tempDirRootChanged(QString root); + void backgroundModeChanged(int mode); + + void tempDirButtonClicked(); + + void okClicked(); + void applyClicked(); + void cancelClicked(); + +protected: + WindowTypeSelector *m_windowTypeSelector; + QPushButton *m_applyButton; + + QLineEdit *m_tempDirRootEdit; + + WindowType m_windowType; + int m_spectrogramSmoothing; + int m_propertyLayout; + float m_tuningFrequency; + int m_resampleQuality; + bool m_resampleOnLoad; + QString m_tempDirRoot; + int m_backgroundMode; + + bool m_changesOnRestart; +}; + +#endif diff --git a/main/main.cpp b/main/main.cpp new file mode 100644 index 0000000..f42f08f --- /dev/null +++ b/main/main.cpp @@ -0,0 +1,237 @@ +/* -*- 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 and QMUL. + + 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 "system/System.h" +#include "system/Init.h" +#include "base/TempDirectory.h" +#include "base/PropertyContainer.h" +#include "base/Preferences.h" +#include "widgets/TipDialog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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 +} + +class VectApplication : public QApplication +{ +public: + VectApplication(int argc, char **argv) : + QApplication(argc, argv), + m_mainWindow(0) { } + virtual ~VectApplication() { } + + void setMainWindow(MainWindow *mw) { m_mainWindow = mw; } + void releaseMainWindow() { m_mainWindow = 0; } + + virtual void commitData(QSessionManager &manager) { + if (!m_mainWindow) return; + bool mayAskUser = manager.allowsInteraction(); + bool success = m_mainWindow->commitData(mayAskUser); + manager.release(); + if (!success) manager.cancel(); + } + +protected: + MainWindow *m_mainWindow; +}; + +int +main(int argc, char **argv) +{ + VectApplication application(argc, argv); + + QStringList args = application.arguments(); + + signal(SIGINT, signalHandler); + signal(SIGTERM, signalHandler); + +#ifndef Q_WS_WIN32 + signal(SIGHUP, signalHandler); + signal(SIGQUIT, signalHandler); +#endif + + svSystemSpecificInitialisation(); + + bool audioOutput = true; + bool oscSupport = true; + + if (args.contains("--help") || args.contains("-h") || args.contains("-?")) { + std::cerr << QApplication::tr( + "\nSonic Visualiser is a program for viewing and exploring audio data\nfor semantic music analysis and annotation.\n\nUsage:\n\n %1 [--no-audio] [--no-osc] [ ...]\n\n --no-audio: Do not attempt to open an audio output device\n --no-osc: Do not provide an Open Sound Control port for remote control\n : One or more Sonic Visualiser (.sv) and audio files may be provided.\n").arg(argv[0]).toStdString() << std::endl; + exit(2); + } + + if (args.contains("--no-audio")) audioOutput = false; + if (args.contains("--no-osc")) oscSupport = false; + + QApplication::setOrganizationName("sonic-visualiser"); + QApplication::setOrganizationDomain("sonicvisualiser.org"); + QApplication::setApplicationName("vect"); + + QIcon icon; + int sizes[] = { 16, 22, 24, 32, 48, 64, 128 }; + for (int i = 0; i < sizeof(sizes)/sizeof(sizes[0]); ++i) { + icon.addFile(QString(":icons/sv-%1x%2.png").arg(sizes[i]).arg(sizes[i])); + } + QApplication::setWindowIcon(icon); + + QString language = QLocale::system().name(); + + QTranslator qtTranslator; + QString qtTrName = QString("qt_%1").arg(language); + std::cerr << "Loading " << qtTrName.toStdString() << "..." << std::endl; + bool success = false; + if (!(success = qtTranslator.load(qtTrName))) { + QString qtDir = getenv("QTDIR"); + if (qtDir != "") { + success = qtTranslator.load + (qtTrName, QDir(qtDir).filePath("translations")); + } + } + if (!success) { + std::cerr << "Failed to load Qt translation for locale" << std::endl; + } + application.installTranslator(&qtTranslator); + + //!!! load sv translations, plus vect translations + 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(audioOutput, oscSupport); + application.setMainWindow(&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; + + QSettings settings; + settings.beginGroup("MainWindow"); + QSize size = settings.value("size", QSize(width, height)).toSize(); + gui.resize(size); + if (settings.contains("position")) { + gui.move(settings.value("position").toPoint()); + } + settings.endGroup(); + + gui.show(); + + bool haveSession = false; + bool haveMainModel = false; + bool havePriorCommandLineModel = false; + + for (QStringList::iterator i = args.begin(); i != args.end(); ++i) { + + MainWindow::FileOpenStatus status = MainWindow::FileOpenFailed; + + if (i == args.begin()) continue; + if (i->startsWith('-')) continue; + + if (i->startsWith("http:") || i->startsWith("ftp:")) { + std::cerr << "opening URL: \"" << i->toStdString() << "\"..." << std::endl; + status = gui.openURL(*i); + continue; + } + + QString path = *i; + + if (path.endsWith("sv")) { + if (!haveSession) { + status = gui.openSessionFile(path); + if (status == MainWindow::FileOpenSucceeded) { + haveSession = true; + haveMainModel = true; + } + } else { + std::cerr << "WARNING: Ignoring additional session file argument \"" << path.toStdString() << "\"" << std::endl; + status = MainWindow::FileOpenSucceeded; + } + } + if (status != MainWindow::FileOpenSucceeded) { + if (!haveMainModel) { + status = gui.openSomeFile(path, MainWindow::ReplaceMainModel); + if (status == MainWindow::FileOpenSucceeded) { + haveMainModel = true; + } + } else { + if (haveSession && !havePriorCommandLineModel) { + status = gui.openSomeFile(path, MainWindow::AskUser); + if (status == MainWindow::FileOpenSucceeded) { + havePriorCommandLineModel = true; + } + } else { + status = gui.openSomeFile(path, MainWindow::CreateAdditionalModel); + } + } + } + if (status == MainWindow::FileOpenFailed) { + QMessageBox::critical + (&gui, QMessageBox::tr("Failed to open file"), + QMessageBox::tr("File \"%1\" could not be opened").arg(path)); + } + } + + + +/* + TipDialog tipDialog; + if (tipDialog.isOK()) { + tipDialog.exec(); + } +*/ + int rv = application.exec(); +// std::cerr << "application.exec() returned " << rv << std::endl; + + cleanupMutex.lock(); + TempDirectory::getInstance()->cleanup(); + application.releaseMainWindow(); + + return rv; +} diff --git a/osc/OSCMessage.cpp b/osc/OSCMessage.cpp new file mode 100644 index 0000000..a60b596 --- /dev/null +++ b/osc/OSCMessage.cpp @@ -0,0 +1,52 @@ +/* -*- 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 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. +*/ + +/* + This is a modified version of a source file from the + Rosegarden MIDI and audio sequencer and notation editor. + This file copyright 2000-2006 Chris Cannam. +*/ + +#include "OSCMessage.h" + + +OSCMessage::~OSCMessage() +{ + clearArgs(); +} + +void +OSCMessage::clearArgs() +{ + m_args.clear(); +} + +void +OSCMessage::addArg(QVariant arg) +{ + m_args.push_back(arg); +} + +size_t +OSCMessage::getArgCount() const +{ + return m_args.size(); +} + +const QVariant & +OSCMessage::getArg(size_t i) const +{ + return m_args[i]; +} + diff --git a/osc/OSCMessage.h b/osc/OSCMessage.h new file mode 100644 index 0000000..da92c07 --- /dev/null +++ b/osc/OSCMessage.h @@ -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 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. +*/ + +/* + This is a modified version of a source file from the + Rosegarden MIDI and audio sequencer and notation editor. + This file copyright 2000-2006 Chris Cannam. +*/ + +#ifndef _OSC_MESSAGE_H_ +#define _OSC_MESSAGE_H_ + +#include +#include + +#include +#include + +class OSCMessage +{ +public: + OSCMessage() { } + ~OSCMessage(); + + void setTarget(const int &target) { m_target = target; } + int getTarget() const { return m_target; } + + void setTargetData(const int &targetData) { m_targetData = targetData; } + int getTargetData() const { return m_targetData; } + + void setMethod(QString method) { m_method = method; } + QString getMethod() const { return m_method; } + + void clearArgs(); + void addArg(QVariant arg); + + size_t getArgCount() const; + const QVariant &getArg(size_t i) const; + +private: + int m_target; + int m_targetData; + QString m_method; + std::vector m_args; +}; + +#endif diff --git a/osc/OSCQueue.cpp b/osc/OSCQueue.cpp new file mode 100644 index 0000000..87163c4 --- /dev/null +++ b/osc/OSCQueue.cpp @@ -0,0 +1,222 @@ +/* -*- 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 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. +*/ + +/* + This is a modified version of a source file from the + Rosegarden MIDI and audio sequencer and notation editor. + This file copyright 2000-2006 Chris Cannam and QMUL. +*/ + +#include "OSCQueue.h" + +#include + +#define OSC_MESSAGE_QUEUE_SIZE 1023 + +#ifdef HAVE_LIBLO + +void +OSCQueue::oscError(int num, const char *msg, const char *path) +{ + std::cerr << "ERROR: OSCQueue::oscError: liblo server error " << num + << " in path " << path << ": " << msg << std::endl; +} + +int +OSCQueue::oscMessageHandler(const char *path, const char *types, lo_arg **argv, + int argc, lo_message, void *user_data) +{ + OSCQueue *queue = static_cast(user_data); + + int target; + int targetData; + QString method; + + if (!queue->parseOSCPath(path, target, targetData, method)) { + return 1; + } + + OSCMessage message; + message.setTarget(target); + message.setTargetData(targetData); + message.setMethod(method); + + int i = 0; + + while (types && i < argc && types[i]) { + + char type = types[i]; + lo_arg *arg = argv[i]; + + switch (type) { + case 'i': message.addArg(arg->i); break; + // This conversion fails to compile in 64-bit environments + // at present, and we don't use the h type anyway so we + // can safely omit it +// case 'h': message.addArg(arg->h); break; + case 'f': message.addArg(arg->f); break; + case 'd': message.addArg(arg->d); break; + case 'c': message.addArg(arg->c); break; + case 't': message.addArg(arg->i); break; + case 's': message.addArg(&arg->s); break; + default: std::cerr << "WARNING: OSCQueue::oscMessageHandler: " + << "Unsupported OSC type '" << type << "'" + << std::endl; + break; + } + + ++i; + } + + queue->postMessage(message); + return 0; +} + +#endif + +OSCQueue::OSCQueue() : +#ifdef HAVE_LIBLO + m_thread(0), +#endif + m_buffer(OSC_MESSAGE_QUEUE_SIZE) +{ +#ifdef HAVE_LIBLO + m_thread = lo_server_thread_new(NULL, oscError); + + lo_server_thread_add_method(m_thread, NULL, NULL, + oscMessageHandler, this); + + lo_server_thread_start(m_thread); + + std::cout << "OSCQueue::OSCQueue: Base OSC URL is " + << lo_server_thread_get_url(m_thread) << std::endl; +#endif +} + +OSCQueue::~OSCQueue() +{ +#ifdef HAVE_LIBLO + if (m_thread) { + lo_server_thread_stop(m_thread); + } +#endif + + while (m_buffer.getReadSpace() > 0) { + delete m_buffer.readOne(); + } +} + +bool +OSCQueue::isOK() const +{ +#ifdef HAVE_LIBLO + return (m_thread != 0); +#else + return false; +#endif +} + +QString +OSCQueue::getOSCURL() const +{ + QString url = ""; +#ifdef HAVE_LIBLO + url = lo_server_thread_get_url(m_thread); +#endif + return url; +} + +size_t +OSCQueue::getMessagesAvailable() const +{ + return m_buffer.getReadSpace(); +} + +OSCMessage +OSCQueue::readMessage() +{ + OSCMessage *message = m_buffer.readOne(); + OSCMessage rmessage = *message; + delete message; + return rmessage; +} + +void +OSCQueue::postMessage(OSCMessage message) +{ + int count = 0, max = 5; + while (m_buffer.getWriteSpace() == 0) { + if (count == max) { + std::cerr << "ERROR: OSCQueue::postMessage: OSC message queue is full and not clearing -- abandoning incoming message" << std::endl; + return; + } + std::cerr << "WARNING: OSCQueue::postMessage: OSC message queue (capacity " << m_buffer.getSize() << " is full!" << std::endl; + std::cerr << "Waiting for something to be processed" << std::endl; +#ifdef _WIN32 + Sleep(1); +#else + sleep(1); +#endif + count++; + } + + OSCMessage *mp = new OSCMessage(message); + m_buffer.write(&mp, 1); + std::cerr << "OSCQueue::postMessage: Posted OSC message: target " + << message.getTarget() << ", target data " << message.getTargetData() + << ", method " << message.getMethod().toStdString() << std::endl; + emit messagesAvailable(); +} + +bool +OSCQueue::parseOSCPath(QString path, int &target, int &targetData, + QString &method) +{ + while (path.startsWith("/")) { + path = path.right(path.length()-1); + } + + int i = 0; + + bool ok = false; + target = path.section('/', i, i).toInt(&ok); + + if (!ok) { + target = 0; + } else { + ++i; + targetData = path.section('/', i, i).toInt(&ok); + if (!ok) { + targetData = 0; + } else { + ++i; + } + } + + method = path.section('/', i, -1); + + if (method.contains('/')) { + std::cerr << "ERROR: OSCQueue::parseOSCPath: malformed path \"" + << path.toStdString() << "\" (should be target/data/method or " + << "target/method or method, where target and data " + << "are numeric)" << std::endl; + return false; + } + + std::cerr << "OSCQueue::parseOSCPath: good path \"" << path.toStdString() + << "\"" << std::endl; + + return true; +} + diff --git a/osc/OSCQueue.h b/osc/OSCQueue.h new file mode 100644 index 0000000..fe75308 --- /dev/null +++ b/osc/OSCQueue.h @@ -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 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. +*/ + +/* + This is a modified version of a source file from the + Rosegarden MIDI and audio sequencer and notation editor. + This file copyright 2000-2006 Chris Cannam and QMUL. +*/ + +#ifndef _OSC_QUEUE_H_ +#define _OSC_QUEUE_H_ + +#include "OSCMessage.h" + +#include "base/RingBuffer.h" + +#include + +#ifdef HAVE_LIBLO +#include +#endif + +class OSCQueue : public QObject +{ + Q_OBJECT + +public: + OSCQueue(); + virtual ~OSCQueue(); + + bool isOK() const; + + bool isEmpty() const { return getMessagesAvailable() == 0; } + size_t getMessagesAvailable() const; + OSCMessage readMessage(); + + QString getOSCURL() const; + +signals: + void messagesAvailable(); + +protected: +#ifdef HAVE_LIBLO + lo_server_thread m_thread; + + static void oscError(int, const char *, const char *); + static int oscMessageHandler(const char *, const char *, lo_arg **, + int, lo_message, void *); +#endif + + void postMessage(OSCMessage); + bool parseOSCPath(QString path, int &target, int &targetData, QString &method); + + RingBuffer m_buffer; +}; + +#endif + diff --git a/osc/demoscript.sh b/osc/demoscript.sh new file mode 100755 index 0000000..f1ae713 --- /dev/null +++ b/osc/demoscript.sh @@ -0,0 +1,541 @@ +#!/bin/bash + +audio=/data/music +preferred=$audio/free +list=audiofiles.txt +used=audiofiles-used.txt + +df=vamp:vamp-aubio:aubioonset:detectionfunction +#df=vamp:qm-vamp-plugins:qm-tempotracker:detection_fn +onsets=vamp:vamp-aubio:aubioonset:onsets +#onsets=vamp:qm-vamp-plugins:qm-tempotracker:beats +beats=vamp:vamp-aubio:aubiotempo:beats +#beats=$onsets +#onsets=$beats +chromagram=vamp:qm-vamp-plugins:qm-chromagram:chromagram +notes=vamp:vamp-aubio:aubionotes:notes + +pid=`cat /tmp/demoscript.pid 2>/dev/null` +if [ -n "$pid" ]; then + kill "$pid" +fi +echo $$ > /tmp/demoscript.pid +trap "rm /tmp/demoscript.pid" 0 + +sv-command quit +sleep 1 +killall -9 sonic-visualiser +sleep 1 + +pick_file() +{ + file="" + count=`wc -l "$list" 2>/dev/null | awk '{ print $1 }'` + if [ ! -f "$list" ] || [ "$count" -eq "0" ] ; then + find "$audio" -name \*.ogg -print >> "$list" + find "$audio" -name \*.mp3 -print >> "$list" + find "$audio" -name \*.wav -print >> "$list" + find "$preferred" -name \*.ogg -print >> "$list" + find "$preferred" -name \*.mp3 -print >> "$list" + find "$preferred" -name \*.wav -print >> "$list" + count=`wc -l "$list" 2>/dev/null | awk '{ print $1 }'` + fi + while [ -z "$file" ]; do + index=$((RANDOM % $count)) + file=`tail +"$index" "$list" | head -1` + [ -f "$file" ] || continue + done + fgrep -v "$file" "$list" > "$list"_ && mv "$list"_ "$list" + echo "$file" +} + +load_a_file() +{ + file=`pick_file` + if ! sv-command open "$file"; then + pid="`pidof sonic-visualiser`" + if [ -z "$pid" ]; then + ( setsid sonic-visualiser -geometry 1000x500+10+100 & ) + sleep 2 + sudo renice +19 `pidof sonic-visualiser` + sudo renice +18 `pidof Xorg` + sv-command resize 1000 500 + load_a_file + else + echo "ERROR: Unable to contact sonic-visualiser pid $pid" 1>&2 + exit 1 + fi + fi +} + +show_stuff() +{ + sv-command set overlays 2 +# sv-command set zoomwheels 1 + sv-command set propertyboxes 1 +} + +hide_stuff() +{ + sv-command set overlays 0 +# sv-command set zoomwheels 0 + sv-command set propertyboxes 0 +} + +reset() +{ + for pane in 1 2 3 4 5; do + for layer in 1 2 3 4 5 6 7 8 9 10; do + sv-command delete layer + done + sv-command delete pane + done + sv-command zoom default + sv-command add waveform + show_stuff +} + +scroll_and_zoom() +{ + sv-command set overlays 0 + sv-command set zoomwheels 0 + sv-command set propertyboxes 0 +# sv-command setcurrent 1 1 +# sv-command delete layer +# sv-command setcurrent 1 1 + sv-command set layer Colour Red + sleep 1 + sv-command set pane Global-Zoom off + sv-command set pane Global-Scroll off + sv-command set pane Follow-Playback Scroll + for zoom in 950 900 850 800 750 700 650 600 550 512 450 400 350 300 256 192 160 128 96 64 48 32 24 16; do + sv-command zoom $zoom + sleep 0.1 + done +} + +play() +{ + sv-command play "$@" +} + +fade_in() +{ + sv-command set gain 0 + sleep 0.5 + play "$@" + for gain in 0.001 0.01 0.05 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1; do + sv-command set gain $gain + sleep 0.1 + done +} + +fade_out() +{ + for gain in 0.9 0.8 0.7 0.6 0.5 0.4 0.3 0.2 0.1 0.05 0.01 0.001; do + sv-command set gain $gain + sleep 0.1 + done + stop + sv-command set gain 1 +} + +slow() +{ +# for speed in -1 -10 -20 -30 -40 -50 -60 -70 -80 -100 -140 -200 -250 -300 -400 -500 -700 -800 -900 -1000; do +# sv-command set speedup "$speed" +# sleep 1 +# done + for speed in -20 -100 -1000; do + sv-command set speedup "$speed" + sleep 10 + done +} + +stop() +{ + sv-command stop "$@" + sv-command set speedup 0 +} + +quit() +{ + sv-command quit +} + +add_melodic_range_spectrogram() +{ + sv-command set propertyboxes 1 + sv-command add spectrogram + sv-command set layer Window-Size 8192 +# sv-command set layer Window-Size 4096 + sv-command set layer Window-Overlap 4 +# sv-command set layer Window-Overlap 3 + sv-command set layer Frequency-Scale Log + sv-command set layer Colour-Scale Meter +} + +zoom_in_spectrogram() +{ + sv-command zoomvertical 43 8000 + for x in 1 2 3 4 5 6; do + max=$((8000 - 1000*$x)) + sv-command zoomvertical 43 "$max" + sleep 0.5 + done + for x in 1 2 3 4 5; do + max=$((2000 - 100 * $x)) + sv-command zoomvertical 43 "$max" + sleep 0.5 + done +} + +zoom_in_spectrogram_further() +{ + for x in 1 2 3 4 5; do + sv-command zoomvertical in + done +} + +playback_bits() +{ + sv-command setcurrent 1 + sv-command set pane Global-Zoom off + sv-command set pane Global-Scroll off + sv-command set pane Follow-Playback Scroll + sv-command jump 10 + sv-command setcurrent 1 1 + sv-command delete layer + sv-command setcurrent 1 1 +# sv-command setcurrent 1 2 + sv-command set layer Colour Blue + sleep 5 + hide_stuff + sv-command set overlays 0 + sv-command set zoomwheels 0 + sv-command set propertyboxes 0 + fade_in + sleep 10 +# sv-command set layer Colour Blue +# sleep 1 +# sv-command set layer Colour Orange +# sleep 1 +# sv-command set layer Colour Red +# sleep 1 +# sv-command set layer Colour Green +# sleep 1 +# sleep 1 + + +# scroll_and_zoom + +# sv-command set overlays 0 +# sv-command set zoomwheels 0 +# sv-command set propertyboxes 0 +# sv-command setcurrent 1 1 +# sv-command delete layer +# sv-command setcurrent 1 1 +# sv-command set layer Colour Red +# sleep 1 +# sv-command set pane Global-Zoom off +# sv-command set pane Global-Scroll off +# sv-command set pane Follow-Playback Scroll + sv-command set zoomwheels 1 + sleep 1 + for zoom in 950 900 850 800 750 700 650 600 550 512 450 400 350 300 256 192 160 128 96 64 48 32 24 16; do + sv-command zoom $zoom + sleep 0.1 + done + + sleep 1 + sv-command set zoomwheels 0 + sv-command zoom 16 + + sleep 10 + #slow + #sv-command set layer Normalize-Visible-Area on +# for zoom in 15 14 13 12 11 10 9 8 7 6 5 4 ; do +# sv-command zoom $zoom +# sleep 0.1 + # done + sleep 1 + sv-command set zoomwheels 0 + slow + sleep 7 + fade_out + sv-command setcurrent 1 + sv-command set pane Follow-Playback Page + sv-command set pane Global-Zoom on + sv-command set pane Global-Scroll on + done_playback_bits=1 +} + +spectrogram_bits() +{ + sv-command set pane Global-Zoom on + sv-command zoom 1024 + add_melodic_range_spectrogram + sv-command zoom 1024 + sleep 5 + sv-command jump 10 + sleep 20 + zoom_in_spectrogram + sleep 20 + + sv-command select 7.5 11 + fade_in selection + sleep 10 + sv-command set speedup -200 + sleep 10 + sv-command setcurrent 1 + sv-command delete pane + sv-command zoom in + sv-command setcurrent 1 2 + sv-command set layer Normalize-Columns off + sv-command set layer Normalize-Visible-Area on + sleep 20 + sv-command set speedup 0 + sleep 10 + sv-command select none +# fade_out + +# if [ -n "$done_playback_bits" ]; then +# sv-command setcurrent 1 +# sv-command zoom out +# sv-command zoom outvamp:qm-vamp-plugins:qm-chromagram:chromagram +# sv-command zoom out +# sv-command zoom out +# sv-command zoom out +# sv-command setcurrent 2 +# fi + +# hide_stuff +# fade_in + sleep 10 +# sv-command set layer Bin-Display Frequencies +# sv-command set layer Normalize-Columns on +# sleep 20 + sv-command set layer Bin-Display "All Bins" + sv-command set layer Normalize-Columns on + sv-command set layer Normalize-Visible-Area off + sv-command set layer Colour-Scale 0 + sv-command set layer Colour "Red on Blue" + sv-command zoomvertical 23 800 + sleep 20 + sv-command transform $onsets + sv-command set layer Colour Orange + sleep 20 + fade_out + sleep 1 +# sv-command jump 10 +# sv-command setcurrent 1 2 +# sv-command set layer Colour "Black on White" +# sv-command transform $notes +# sv-command set layer Colour Orange + sleep 10 +# sv-command setcurrent 1 3 +# sv-command delete layer + sv-command setcurrent 1 3 + sv-command delete layer + sv-command setcurrent 1 2 + sv-command set layer Colour Default + done_spectrogram_bits=1 + +# zoom_in_spectrogram_further +} + +onset_bits() +{ + show_stuff + sv-command set zoomwheels 0 + sv-command setcurrent 1 + sv-command set pane Global-Zoom on + sv-command set pane Global-Scroll on + sleep 0.5 + sv-command set layer Colour Blue + sleep 0.5 + sv-command set layer Colour Orange + sleep 0.5 + sv-command set layer Colour Red + sleep 0.5 + sv-command set layer Colour Green + sleep 1 +# sleep 1 +# if [ -n "$done_spectrogram_bits" ]; then +# sv-command setcurrent 2 +# sv-command delete pane +# fi +# sv-command zoom default +# sv-command zoom in +# sv-command zoom in +# sv-command zoom in + sv-command zoom 192 + sv-command zoom in + sv-command add timeruler + sv-command jump 0 + sv-command transform $df + sv-command set layer Colour Black + sleep 5 + sv-command set layer Plot-Type Curve + sleep 5 + sv-command jump 30 + sv-command setcurrent 1 + sv-command set pane Follow-Playback Page + sv-command transform $df + sv-command set layer Colour Red + sleep 5 + sv-command jump 30 + sleep 5 + if [ "$RANDOM" -lt 16384 ]; then + sv-command set layer Vertical-Scale "Log Scale" + fi + sv-command set layer Plot-Type Segmentation + sleep 5 +# hide_stuff + sleep 10 + sv-command set overlays 0 + sv-command set propertyboxes 0 +# sv-command setcurrent 1 1 +# sv-command set layer Colour Black +# sv-command setcurrent 1 2 + sleep 2 + fade_in + sleep 2 + sv-command transform $onsets + sv-command set layer Colour Black + sv-command setcurrent 2 + sv-command transform $onsets + sv-command set layer Colour Blue + sleep 20 +# sv-command setcurrent 2 +# sv-command transform vamp:qm-vamp-plugins:qm-tempotracker:beats +# sv-command transform $beats + sleep 20 +# fade_out +# show_stuff +} + +selection_bits() +{ +# reset + sv-command set overlays 1 + sv-command set zoomwheels 0 + sv-command resize 1000 500 + sv-command zoom default + sv-command setcurrent 2 + sv-command delete pane +# if [ -n "$done_playback_bits" ]; then + sv-command setcurrent 1 2 +# else +# sv-command setcurrent 1 3 +# fi + sv-command delete layer +# if [ -n "$done_playback_bits" ]; then + sv-command setcurrent 1 2 +# else +# sv-command setcurrent 1 3 +# fi + sv-command delete layer + sv-command setcurrent 1 2 + sv-command set layer Colour Orange +# sv-command transform vamp:qm-vamp-plugins:qm-tempotracker:beats + sv-command transform $beats +# sv-command setcurrent 1 2 + sv-command set layer Colour Black + sleep 20 + sv-command loop on + base=$((RANDOM % 100)) + sv-command select $base $base.3 +# fade_in selection + play selection + sleep 8 + base=$((base + 4)) + sv-command addselect $base $base.1 + #sleep 12 + base=$((base + 2)) + sv-command addselect $base $base.1 + #sleep 6 + base=$((base + 2)) + sv-command addselect $base $base.3 + #sleep 6 + base=$((base + 3)) + sv-command addselect $base $base.3 + #sleep 6 + base=$((base + 2)) + sv-command addselect $base $base.3 + sleep 4 + sv-command delete layer + sleep 16 + sv-command set speedup -50 + sleep 14 + sv-command set speedup 50 + sleep 8 + sv-command set speedup 100 + sleep 5 + sv-command set speedup 200 + fade_out +# sleep 10 + sv-command select none + sv-command set overlays 2 + sv-command set propertyboxes 1 +# sv-command setcurrent 1 3 +# sv-command delete layer + sv-command setcurrent 1 2 + sv-command set layer Colour Black +} + +chromagram_bits() +{ +# add_melodic_range_spectrogram +# sleep 10 + sv-command add timeruler + sleep 5 + sv-command jump 10 + sv-command zoom out + sleep 5 + sv-command transform $chromagram + sleep 40 + sv-command zoom out + fade_in + sleep 20 + fade_out +} + +while /bin/true; do + +sleep 2 +load_a_file +sv-command loop on + +sv-command resize 1000 500 +show_stuff +sleep 5 +sleep 20 +playback_bits + +#sleep 10 +sv-command resize 1000 700 +sv-command zoom default +show_stuff +onset_bits + +selection_bits + +#sv-command resize 1000 700 + +#sleep 10 +sv-command resize 1000 700 +#show_stuff +spectrogram_bits + +#sleep 10 +#sv-command jump 0 +#show_stuff +#chromagram_bits + +sleep 20 + +#reset +killall -9 sonic-visualiser + +done diff --git a/osc/sv-command b/osc/sv-command new file mode 100755 index 0000000..6b89a75 --- /dev/null +++ b/osc/sv-command @@ -0,0 +1,55 @@ +#!/bin/sh +# +# A very simple command shell for Sonic Visualiser. +# +# This provides a wrapper for the sv-osc-send program, which is a +# generic OSC sending program (not specific to SV, despite its name). +# This script attempts to guess the OSC port number for an SV +# process running on the local host, and then composes a method name +# and arguments into a complete OSC call. +# +# You can either run this with the method and its arguments on the +# command line, e.g. "sv-command set layer Frequency-Scale Log", or +# you can provide a series of method + argument commands on stdin. +# +# Unless you use the -q option, this script will echo the OSC URL +# and arguments that it is sending for each command. +# +# Note that the method and arguments may not contain spaces. +# +# Chris Cannam, Nov 2006 + +quiet= +if [ "$1" = "-q" ]; then + quiet=true; shift; +fi + +# The yucky bit + +port=`lsof -c sonic- | \ + grep UDP | \ + sed -e 's/^.*[^0-9]\([0-9][0-9]*\) *$/\1/' | \ + grep -v ' ' | \ + head -1 ` + +host=127.0.0.1 +scheme=osc.udp + +if [ -z "$port" ]; then + echo "Sonic Visualiser OSC port not found" + exit 1 +fi + +if [ -n "$1" ]; then + command=$1; shift + [ -z "$quiet" ] && echo "$scheme://$host:$port/$command" "$@" + sv-osc-send "$scheme://$host:$port/$command" "$@" +else + while read command a1 a2 a3 a4 a5; do + [ -z "$command" ] && continue + [ -z "$quiet" ] && echo "$scheme://$host:$port/$command" $a1 $a2 $a3 $a4 $a5 + sv-osc-send "$scheme://$host:$port/$command" $a1 $a2 $a3 $a4 $a5 + done +fi + +exit 0 diff --git a/osc/sv-osc-send.c b/osc/sv-osc-send.c new file mode 100644 index 0000000..ac4a2b7 --- /dev/null +++ b/osc/sv-osc-send.c @@ -0,0 +1,73 @@ +/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */ + +#include +#include +#include +#include +#include + +void +usage(char *program_name) +{ + char *base_name = strrchr(program_name, '/'); + + if (base_name && *(base_name + 1) != 0) { + base_name += 1; + } else { + base_name = program_name; + } + + fprintf(stderr, "\nusage: %s []\n\n", program_name); + fprintf(stderr, "example OSC URLs:\n\n" + " osc.udp://localhost:19383/path/test 1.0 4.2\n" + " osc.udp://my.host.org:10886/3/13/load file\n\n"); + fprintf(stderr, "numeric arguments will be treated as OSC 'f' floating point types.\n\n"); + exit(1); +} + +int main(int argc, char *argv[]) +{ + lo_address a; + char *url, *host, *port, *path; + lo_message message; + unsigned int i; + + if (argc < 2) { + usage(argv[0]); + /* does not return */ + } + url = argv[1]; + + host = lo_url_get_hostname(url); + port = lo_url_get_port(url); + path = lo_url_get_path(url); + a = lo_address_new(host, port); + + message = lo_message_new(); + + for (i = 0; i + 2 < argc; ++i) { + + int index = i + 2; + char *param; + + param = argv[index]; + if (!isdigit(param[0])) { + lo_message_add_string(message, argv[index]); + } else { + lo_message_add_float(message, atof(argv[index])); + } + } + + lo_send_message(a, path, message); + + if (lo_address_errno(a)) { + printf("liblo error: %s\n", lo_address_errstr(a)); + } + + free(host); + free(port); + free(path); + + return 0; +} + diff --git a/samples/README b/samples/README new file mode 100644 index 0000000..d26cf62 --- /dev/null +++ b/samples/README @@ -0,0 +1,2 @@ +Samples from standard Hydrogen drum machine kits, except for piano.wav +(ancestry unknown) and click.wav (random noise). diff --git a/samples/bass.wav b/samples/bass.wav new file mode 100644 index 0000000..7f9a009 Binary files /dev/null and b/samples/bass.wav differ diff --git a/samples/beep.wav b/samples/beep.wav new file mode 100644 index 0000000..806aaa0 Binary files /dev/null and b/samples/beep.wav differ diff --git a/samples/bounce.wav b/samples/bounce.wav new file mode 100644 index 0000000..3d1c880 Binary files /dev/null and b/samples/bounce.wav differ diff --git a/samples/clap.wav b/samples/clap.wav new file mode 100644 index 0000000..e00e4bf Binary files /dev/null and b/samples/clap.wav differ diff --git a/samples/click.wav b/samples/click.wav new file mode 100644 index 0000000..b9e6965 Binary files /dev/null and b/samples/click.wav differ diff --git a/samples/cowbell.wav b/samples/cowbell.wav new file mode 100644 index 0000000..bf701b9 Binary files /dev/null and b/samples/cowbell.wav differ diff --git a/samples/hihat.wav b/samples/hihat.wav new file mode 100644 index 0000000..3098f5f Binary files /dev/null and b/samples/hihat.wav differ diff --git a/samples/kick.wav b/samples/kick.wav new file mode 100644 index 0000000..0de4316 Binary files /dev/null and b/samples/kick.wav differ diff --git a/samples/organ.wav b/samples/organ.wav new file mode 100644 index 0000000..979fd69 Binary files /dev/null and b/samples/organ.wav differ diff --git a/samples/piano.wav b/samples/piano.wav new file mode 100644 index 0000000..9a4c21f Binary files /dev/null and b/samples/piano.wav differ diff --git a/samples/snare.wav b/samples/snare.wav new file mode 100644 index 0000000..9d7d5ad Binary files /dev/null and b/samples/snare.wav differ diff --git a/samples/stick.wav b/samples/stick.wav new file mode 100644 index 0000000..d24f85f Binary files /dev/null and b/samples/stick.wav differ diff --git a/samples/strike.wav b/samples/strike.wav new file mode 100644 index 0000000..4b8910e Binary files /dev/null and b/samples/strike.wav differ diff --git a/samples/tap.wav b/samples/tap.wav new file mode 100644 index 0000000..58e5aa9 Binary files /dev/null and b/samples/tap.wav differ diff --git a/transform/FeatureExtractionPluginTransform.cpp b/transform/FeatureExtractionPluginTransform.cpp new file mode 100644 index 0000000..f35a1c5 --- /dev/null +++ b/transform/FeatureExtractionPluginTransform.cpp @@ -0,0 +1,553 @@ +/* -*- 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 and QMUL. + + 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 "data/model/Model.h" +#include "base/Window.h" +#include "data/model/SparseOneDimensionalModel.h" +#include "data/model/SparseTimeValueModel.h" +#include "data/model/EditableDenseThreeDimensionalModel.h" +#include "data/model/DenseTimeValueModel.h" +#include "data/model/NoteModel.h" +#include "data/model/FFTModel.h" +#include "data/model/WaveFileModel.h" + +#include + +#include + +FeatureExtractionPluginTransform::FeatureExtractionPluginTransform(Model *inputModel, + QString pluginId, + const ExecutionContext &context, + QString configurationXml, + QString outputName) : + PluginTransform(inputModel, context), + m_plugin(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); + } + + 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; + } + + std::cerr << "Initialising feature extraction plugin with channels = " + << channelCount << ", step = " << m_context.stepSize + << ", block = " << m_context.blockSize << std::endl; + + if (!m_plugin->initialise(channelCount, + m_context.stepSize, + m_context.blockSize)) { + std::cerr << "FeatureExtractionPluginTransform: Plugin " + << m_plugin->getIdentifier() << " 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].identifier == 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; + bool haveExtents = false; + + 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; + haveExtents = true; + } + + 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_context.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; + if (haveExtents) { + model = new SparseTimeValueModel + (modelRate, modelResolution, minValue, maxValue, false); + } else { + model = new SparseTimeValueModel + (modelRate, modelResolution, 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; + if (haveExtents) { + model = new NoteModel + (modelRate, modelResolution, minValue, maxValue, false); + } else { + model = new NoteModel + (modelRate, modelResolution, false); + } + model->setScaleUnits(outputs[m_outputFeatureNo].unit.c_str()); + + m_output = model; + + } else { + + EditableDenseThreeDimensionalModel *model = + new EditableDenseThreeDimensionalModel + (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()); + } + model->setBinNames(names); + } + + m_output = model; + } +} + +FeatureExtractionPluginTransform::~FeatureExtractionPluginTransform() +{ + std::cerr << "FeatureExtractionPluginTransform::~FeatureExtractionPluginTransform()" << std::endl; + 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; + + while (!input->isReady()) { +/* + if (dynamic_cast(input)) { + std::cerr << "FeatureExtractionPluginTransform::run: Model is not ready, but it's not a WaveFileModel (it's a " << typeid(input).name() << "), so that's OK" << std::endl; + sleep(2); + break; // no need to wait + } +*/ + std::cerr << "FeatureExtractionPluginTransform::run: Waiting for input model to be ready..." << std::endl; + sleep(1); + } + + 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_context.blockSize + 2]; + } + + bool frequencyDomain = (m_plugin->getInputDomain() == + Vamp::Plugin::FrequencyDomain); + std::vector fftModels; + + if (frequencyDomain) { + for (size_t ch = 0; ch < channelCount; ++ch) { + FFTModel *model = new FFTModel + (getInput(), + channelCount == 1 ? m_context.channel : ch, + m_context.windowType, + m_context.blockSize, + m_context.stepSize, + m_context.blockSize, + false); + if (!model->isOK()) { + QMessageBox::critical + (0, tr("FFT cache failed"), + tr("Failed to create the FFT model for this transform.\n" + "There may be insufficient memory or disc space to continue.")); + delete model; + setCompletion(100); + return; + } + model->resume(); + fftModels.push_back(model); + } + } + + long startFrame = m_input->getStartFrame(); + long endFrame = m_input->getEndFrame(); + + long contextStart = m_context.startFrame; + long contextDuration = m_context.duration; + + if (contextStart == 0 || contextStart < startFrame) { + contextStart = startFrame; + } + + if (contextDuration == 0) { + contextDuration = endFrame - contextStart; + } + if (contextStart + contextDuration > endFrame) { + contextDuration = endFrame - contextStart; + } + + long blockFrame = contextStart; + + long prevCompletion = 0; + + setCompletion(0); + + while (!m_abandoned) { + + if (frequencyDomain) { + if (blockFrame - int(m_context.blockSize)/2 > + contextStart + contextDuration) break; + } else { + if (blockFrame >= + contextStart + contextDuration) break; + } + +// std::cerr << "FeatureExtractionPluginTransform::run: blockFrame " +// << blockFrame << ", endFrame " << endFrame << ", blockSize " +// << m_context.blockSize << std::endl; + + long completion = + (((blockFrame - contextStart) / m_context.stepSize) * 99) / + (contextDuration / m_context.stepSize); + + // channelCount is either m_input->channelCount or 1 + + for (size_t ch = 0; ch < channelCount; ++ch) { + if (frequencyDomain) { + int column = (blockFrame - startFrame) / m_context.stepSize; + for (size_t i = 0; i <= m_context.blockSize/2; ++i) { + fftModels[ch]->getValuesAt + (column, i, buffers[ch][i*2], buffers[ch][i*2+1]); + } + } else { + getFrames(ch, channelCount, + blockFrame, m_context.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 == contextStart || completion > prevCompletion) { + setCompletion(completion); + prevCompletion = completion; + } + + blockFrame += m_context.stepSize; + } + + if (m_abandoned) return; + + 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 fftModels[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_context.channel : channel), + startFrame, startFrame + size, buffer + offset); + + while (got < size) { + buffer[offset + got] = 0.0; + ++got; + } + + if (m_context.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, + lrintf(m_descriptor->sampleRate)); + } else { + frame = m_output->getEndFrame(); + } + } + + 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())); +// std::cerr << "SparseTimeValueModel::addPoint(" << frame << ", " << value << "), " << feature.label.c_str() << std::endl; + + } 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, + lrintf(duration), + feature.label.c_str())); + + } else { + + DenseThreeDimensionalModel::Column values = feature.values; + + EditableDenseThreeDimensionalModel *model = + getOutput(); + if (!model) return; + + model->setColumn(frame / model->getResolution(), values); + } +} + +void +FeatureExtractionPluginTransform::setCompletion(int completion) +{ + int binCount = 1; + if (m_descriptor->hasFixedBinCount) { + binCount = m_descriptor->binCount; + } + + std::cerr << "FeatureExtractionPluginTransform::setCompletion(" + << completion << ")" << std::endl; + + 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 { + + EditableDenseThreeDimensionalModel *model = + getOutput(); + if (!model) return; + model->setCompletion(completion); + } +} + diff --git a/transform/FeatureExtractionPluginTransform.h b/transform/FeatureExtractionPluginTransform.h new file mode 100644 index 0000000..da34615 --- /dev/null +++ b/transform/FeatureExtractionPluginTransform.h @@ -0,0 +1,62 @@ +/* -*- 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 and QMUL. + + 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 "PluginTransform.h" + +class DenseTimeValueModel; + +class FeatureExtractionPluginTransform : public PluginTransform +{ + Q_OBJECT + +public: + FeatureExtractionPluginTransform(Model *inputModel, + QString plugin, + const ExecutionContext &context, + QString configurationXml = "", + QString outputName = ""); + virtual ~FeatureExtractionPluginTransform(); + +protected: + virtual void run(); + + Vamp::Plugin *m_plugin; + 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 --git a/transform/PluginTransform.cpp b/transform/PluginTransform.cpp new file mode 100644 index 0000000..f308af2 --- /dev/null +++ b/transform/PluginTransform.cpp @@ -0,0 +1,119 @@ +/* -*- 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 QMUL. + + 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 "PluginTransform.h" + +#include "vamp-sdk/PluginHostAdapter.h" +#include "vamp-sdk/hostext/PluginWrapper.h" + +PluginTransform::PluginTransform(Model *inputModel, + const ExecutionContext &context) : + Transform(inputModel), + m_context(context) +{ +} + +PluginTransform::ExecutionContext::ExecutionContext(int _c, size_t _bs) : + channel(_c), + domain(Vamp::Plugin::TimeDomain), + stepSize(_bs ? _bs : 1024), + blockSize(_bs ? _bs : 1024), + windowType(HanningWindow), + startFrame(0), + duration(0), + sampleRate(0) +{ +} + +PluginTransform::ExecutionContext::ExecutionContext(int _c, size_t _ss, + size_t _bs, WindowType _wt) : + channel(_c), + domain(Vamp::Plugin::FrequencyDomain), + stepSize(_ss ? _ss : (_bs ? _bs / 2 : 512)), + blockSize(_bs ? _bs : 1024), + windowType(_wt), + startFrame(0), + duration(0), + sampleRate(0) +{ +} + +PluginTransform::ExecutionContext::ExecutionContext(int _c, + const Vamp::PluginBase *_plugin) : + channel(_c), + domain(Vamp::Plugin::TimeDomain), + stepSize(0), + blockSize(0), + windowType(HanningWindow), + startFrame(0), + duration(0), + sampleRate(0) +{ + makeConsistentWithPlugin(_plugin); +} + +bool +PluginTransform::ExecutionContext::operator==(const ExecutionContext &c) +{ + return (c.channel == channel && + c.domain == domain && + c.stepSize == stepSize && + c.blockSize == blockSize && + c.windowType == windowType && + c.startFrame == startFrame && + c.duration == duration && + c.sampleRate == sampleRate); +} + +void +PluginTransform::ExecutionContext::makeConsistentWithPlugin(const Vamp::PluginBase *_plugin) +{ + const Vamp::Plugin *vp = dynamic_cast(_plugin); + if (!vp) { +// std::cerr << "makeConsistentWithPlugin: not a Vamp::Plugin" << std::endl; + vp = dynamic_cast(_plugin); //!!! why? +} + if (!vp) { +// std::cerr << "makeConsistentWithPlugin: not a Vamp::PluginHostAdapter" << std::endl; + vp = dynamic_cast(_plugin); //!!! no, I mean really why? + } + if (!vp) { +// std::cerr << "makeConsistentWithPlugin: not a Vamp::HostExt::PluginWrapper" << std::endl; + } + + if (!vp) { + domain = Vamp::Plugin::TimeDomain; + if (!stepSize) { + if (!blockSize) blockSize = 1024; + stepSize = blockSize; + } else { + if (!blockSize) blockSize = stepSize; + } + } else { + domain = vp->getInputDomain(); + if (!stepSize) stepSize = vp->getPreferredStepSize(); + if (!blockSize) blockSize = vp->getPreferredBlockSize(); + if (!blockSize) blockSize = 1024; + if (!stepSize) { + if (domain == Vamp::Plugin::FrequencyDomain) { + stepSize = blockSize/2; + } else { + stepSize = blockSize; + } + } + } +} + + diff --git a/transform/PluginTransform.h b/transform/PluginTransform.h new file mode 100644 index 0000000..13af330 --- /dev/null +++ b/transform/PluginTransform.h @@ -0,0 +1,63 @@ +/* -*- 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 QMUL. + + 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 _PLUGIN_TRANSFORM_H_ +#define _PLUGIN_TRANSFORM_H_ + +#include "Transform.h" + +#include "base/Window.h" + +#include "vamp-sdk/Plugin.h" + +//!!! should this just move back up to Transform? It is after all used +//directly in all sorts of generic places, like Document + +class PluginTransform : public Transform +{ +public: + class ExecutionContext { + public: + // Time domain: + ExecutionContext(int _c = -1, size_t _bs = 0); + + // Frequency domain: + ExecutionContext(int _c, size_t _ss, size_t _bs, WindowType _wt); + + // From plugin defaults: + ExecutionContext(int _c, const Vamp::PluginBase *_plugin); + + bool operator==(const ExecutionContext &); + + void makeConsistentWithPlugin(const Vamp::PluginBase *_plugin); + + int channel; + Vamp::Plugin::InputDomain domain; + size_t stepSize; + size_t blockSize; + WindowType windowType; + size_t startFrame; + size_t duration; // 0 -> whole thing + float sampleRate; // 0 -> model's rate + }; + +protected: + PluginTransform(Model *inputModel, + const ExecutionContext &context); + + ExecutionContext m_context; +}; + +#endif diff --git a/transform/RealTimePluginTransform.cpp b/transform/RealTimePluginTransform.cpp new file mode 100644 index 0000000..56b84ee --- /dev/null +++ b/transform/RealTimePluginTransform.cpp @@ -0,0 +1,255 @@ +/* -*- 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 and QMUL. + + 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 "data/model/Model.h" +#include "data/model/SparseTimeValueModel.h" +#include "data/model/DenseTimeValueModel.h" +#include "data/model/WritableWaveFileModel.h" +#include "data/model/WaveFileModel.h" + +#include + +RealTimePluginTransform::RealTimePluginTransform(Model *inputModel, + QString pluginId, + const ExecutionContext &context, + QString configurationXml, + QString units, + int output) : + PluginTransform(inputModel, context), + m_pluginId(pluginId), + m_configurationXml(configurationXml), + m_units(units), + m_plugin(0), + m_outputNo(output) +{ + if (!m_context.blockSize) m_context.blockSize = 1024; + +// 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(), + m_context.blockSize, + 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 >= 0 && + m_outputNo >= int(m_plugin->getControlOutputCount())) { + std::cerr << "RealTimePluginTransform: Plugin has fewer than desired " << m_outputNo << " control outputs" << std::endl; + return; + } + + if (m_outputNo == -1) { + + size_t outputChannels = m_plugin->getAudioOutputCount(); + if (outputChannels > input->getChannelCount()) { + outputChannels = input->getChannelCount(); + } + + WritableWaveFileModel *model = new WritableWaveFileModel + (input->getSampleRate(), outputChannels); + + m_output = model; + + } else { + + SparseTimeValueModel *model = new SparseTimeValueModel + (input->getSampleRate(), m_context.blockSize, 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; + + while (!input->isReady()) { + if (dynamic_cast(input)) break; // no need to wait + std::cerr << "RealTimePluginTransform::run: Waiting for input model to be ready..." << std::endl; + sleep(1); + } + + SparseTimeValueModel *stvm = dynamic_cast(m_output); + WritableWaveFileModel *wwfm = dynamic_cast(m_output); + if (!stvm && !wwfm) return; + + if (stvm && (m_outputNo >= int(m_plugin->getControlOutputCount()))) return; + + size_t sampleRate = input->getSampleRate(); + size_t channelCount = input->getChannelCount(); + if (!wwfm && m_context.channel != -1) channelCount = 1; + + size_t blockSize = m_plugin->getBufferSize(); + + float **inbufs = m_plugin->getAudioInputBuffers(); + + size_t startFrame = m_input->getStartFrame(); + size_t endFrame = m_input->getEndFrame(); + size_t blockFrame = startFrame; + + size_t prevCompletion = 0; + + size_t latency = m_plugin->getLatency(); + + while (blockFrame < endFrame + latency && !m_abandoned) { + + size_t completion = + (((blockFrame - startFrame) / blockSize) * 99) / + ( (endFrame - startFrame) / blockSize); + + size_t got = 0; + + if (channelCount == 1) { + if (inbufs && inbufs[0]) { + got = input->getValues + (m_context.channel, blockFrame, blockFrame + blockSize, inbufs[0]); + while (got < blockSize) { + inbufs[0][got++] = 0.0; + } + } + for (size_t ch = 1; ch < m_plugin->getAudioInputCount(); ++ch) { + for (size_t i = 0; i < blockSize; ++i) { + inbufs[ch][i] = inbufs[0][i]; + } + } + } else { + for (size_t ch = 0; ch < channelCount; ++ch) { + if (inbufs && inbufs[ch]) { + got = input->getValues + (ch, blockFrame, blockFrame + blockSize, inbufs[ch]); + while (got < blockSize) { + inbufs[ch][got++] = 0.0; + } + } + } + for (size_t ch = channelCount; ch < m_plugin->getAudioInputCount(); ++ch) { + for (size_t i = 0; i < blockSize; ++i) { + inbufs[ch][i] = inbufs[ch % channelCount][i]; + } + } + } + +/* + std::cerr << "Input for plugin: " << m_plugin->getAudioInputCount() << " channels "<< std::endl; + + for (size_t ch = 0; ch < m_plugin->getAudioInputCount(); ++ch) { + std::cerr << "Input channel " << ch << std::endl; + for (size_t i = 0; i < 100; ++i) { + std::cerr << inbufs[ch][i] << " "; + if (isnan(inbufs[ch][i])) { + std::cerr << "\n\nWARNING: NaN in audio input" << std::endl; + } + } + } +*/ + + m_plugin->run(Vamp::RealTime::frame2RealTime(blockFrame, sampleRate)); + + if (stvm) { + + float value = m_plugin->getControlOutputValue(m_outputNo); + + size_t pointFrame = blockFrame; + if (pointFrame > latency) pointFrame -= latency; + else pointFrame = 0; + + stvm->addPoint(SparseTimeValueModel::Point + (pointFrame, value, "")); + + } else if (wwfm) { + + float **outbufs = m_plugin->getAudioOutputBuffers(); + + if (outbufs) { + + if (blockFrame >= latency) { + size_t writeSize = std::min(blockSize, + endFrame + latency - blockFrame); + wwfm->addSamples(outbufs, writeSize); + } else if (blockFrame + blockSize >= latency) { + size_t offset = latency - blockFrame; + size_t count = blockSize - offset; + float **tmp = new float *[channelCount]; + for (size_t c = 0; c < channelCount; ++c) { + tmp[c] = outbufs[c] + offset; + } + wwfm->addSamples(tmp, count); + delete[] tmp; + } + } + } + + if (blockFrame == startFrame || completion > prevCompletion) { + if (stvm) stvm->setCompletion(completion); + if (wwfm) wwfm->setCompletion(completion); + prevCompletion = completion; + } + + blockFrame += blockSize; + } + + if (m_abandoned) return; + + if (stvm) stvm->setCompletion(100); + if (wwfm) wwfm->setCompletion(100); +} + diff --git a/transform/RealTimePluginTransform.h b/transform/RealTimePluginTransform.h new file mode 100644 index 0000000..b67d6ca --- /dev/null +++ b/transform/RealTimePluginTransform.h @@ -0,0 +1,50 @@ +/* -*- 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 and QMUL. + + 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 "PluginTransform.h" +#include "plugin/RealTimePluginInstance.h" + +class DenseTimeValueModel; + +class RealTimePluginTransform : public PluginTransform +{ +public: + RealTimePluginTransform(Model *inputModel, + QString plugin, + const ExecutionContext &context, + QString configurationXml = "", + QString units = "", + int output = -1); // -1 -> audio, 0+ -> data + virtual ~RealTimePluginTransform(); + +protected: + virtual void run(); + + QString m_pluginId; + QString m_configurationXml; + QString m_units; + + RealTimePluginInstance *m_plugin; + int m_outputNo; + + // just casts + DenseTimeValueModel *getInput(); +}; + +#endif + diff --git a/transform/Transform.cpp b/transform/Transform.cpp new file mode 100644 index 0000000..b0cae44 --- /dev/null +++ b/transform/Transform.cpp @@ -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_abandoned(false) +{ +} + +Transform::~Transform() +{ + m_abandoned = true; + wait(); + if (!m_detached) delete m_output; +} + diff --git a/transform/Transform.h b/transform/Transform.h new file mode 100644 index 0000000..dffe41b --- /dev/null +++ b/transform/Transform.h @@ -0,0 +1,61 @@ +/* -*- 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 "base/Thread.h" + +#include "data/model/Model.h" + +typedef QString TransformId; + +/** + * 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(); + + // Just a hint to the processing thread that it should give up. + // Caller should still wait() and/or delete the transform before + // assuming its input and output models are no longer required. + void abandon() { m_abandoned = true; } + + 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_abandoned; +}; + +#endif diff --git a/transform/TransformFactory.cpp b/transform/TransformFactory.cpp new file mode 100644 index 0000000..81041e9 --- /dev/null +++ b/transform/TransformFactory.cpp @@ -0,0 +1,848 @@ +/* -*- 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 and QMUL. + + 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 "data/model/DenseTimeValueModel.h" + +#include "vamp-sdk/PluginHostAdapter.h" + +#include "sv/audioio/AudioCallbackPlaySource.h" //!!! shouldn't include here + +#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(); + + std::set dset; + for (TransformDescriptionMap::const_iterator i = m_transforms.begin(); + i != m_transforms.end(); ++i) { + dset.insert(i->second); + } + + TransformList list; + for (std::set::const_iterator i = dset.begin(); + i != dset.end(); ++i) { + list.push_back(*i); + } + + 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; +} + +std::vector +TransformFactory::getTransformCategories(QString transformType) +{ + if (m_transforms.empty()) populateTransforms(); + + std::set categories; + for (TransformDescriptionMap::const_iterator i = m_transforms.begin(); + i != m_transforms.end(); ++i) { + if (i->second.type == transformType) { + categories.insert(i->second.category); + } + } + + bool haveEmpty = false; + + std::vector rv; + for (std::set::iterator i = categories.begin(); + i != categories.end(); ++i) { + if (*i != "") rv.push_back(*i); + else haveEmpty = true; + } + + if (haveEmpty) rv.push_back(""); // make sure empty category sorts last + + return rv; +} + +std::vector +TransformFactory::getTransformMakers(QString transformType) +{ + if (m_transforms.empty()) populateTransforms(); + + std::set makers; + for (TransformDescriptionMap::const_iterator i = m_transforms.begin(); + i != m_transforms.end(); ++i) { + if (i->second.type == transformType) { + makers.insert(i->second.maker); + } + } + + bool haveEmpty = false; + + std::vector rv; + for (std::set::iterator i = makers.begin(); + i != makers.end(); ++i) { + if (*i != "") rv.push_back(*i); + else haveEmpty = true; + } + + if (haveEmpty) rv.push_back(""); // make sure empty category sorts last + + return rv; +} + +void +TransformFactory::populateTransforms() +{ + TransformDescriptionMap transforms; + + populateFeatureExtractionPlugins(transforms); + populateRealTimePlugins(transforms); + + // disambiguate plugins with similar names + + std::map names; + std::map pluginSources; + std::map pluginMakers; + + for (TransformDescriptionMap::iterator i = transforms.begin(); + i != transforms.end(); ++i) { + + TransformDesc desc = i->second; + + QString td = desc.name; + QString tn = td.section(": ", 0, 0); + QString pn = desc.identifier.section(":", 1, 1); + + if (pluginSources.find(tn) != pluginSources.end()) { + if (pluginSources[tn] != pn && pluginMakers[tn] != desc.maker) { + ++names[tn]; + } + } else { + ++names[tn]; + pluginSources[tn] = pn; + pluginMakers[tn] = desc.maker; + } + } + + std::map counts; + m_transforms.clear(); + + for (TransformDescriptionMap::iterator i = transforms.begin(); + i != transforms.end(); ++i) { + + TransformDesc desc = i->second; + QString identifier = desc.identifier; + QString maker = desc.maker; + + QString td = desc.name; + QString tn = td.section(": ", 0, 0); + QString to = td.section(": ", 1); + + if (names[tn] > 1) { + maker.replace(QRegExp(tr(" [\\(<].*$")), ""); + tn = QString("%1 [%2]").arg(tn).arg(maker); + } + + if (to != "") { + desc.name = QString("%1: %2").arg(tn).arg(to); + } else { + desc.name = tn; + } + + m_transforms[identifier] = 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 pluginName = plugin->getName().c_str(); + QString category = factory->getPluginCategory(pluginId); + + Vamp::Plugin::OutputList outputs = + plugin->getOutputDescriptors(); + + for (size_t j = 0; j < outputs.size(); ++j) { + + QString transformId = QString("%1:%2") + .arg(pluginId).arg(outputs[j].identifier.c_str()); + + QString userName; + QString friendlyName; + QString units = outputs[j].unit.c_str(); + QString description = plugin->getDescription().c_str(); + QString maker = plugin->getMaker().c_str(); + if (maker == "") maker = tr(""); + + if (description == "") { + if (outputs.size() == 1) { + description = tr("Extract features using \"%1\" plugin (from %2)") + .arg(pluginName).arg(maker); + } else { + description = tr("Extract features using \"%1\" output of \"%2\" plugin (from %3)") + .arg(outputs[j].name.c_str()).arg(pluginName).arg(maker); + } + } else { + if (outputs.size() == 1) { + description = tr("%1 using \"%2\" plugin (from %3)") + .arg(description).arg(pluginName).arg(maker); + } else { + description = tr("%1 using \"%2\" output of \"%3\" plugin (from %4)") + .arg(description).arg(outputs[j].name.c_str()).arg(pluginName).arg(maker); + } + } + + if (outputs.size() == 1) { + userName = pluginName; + friendlyName = pluginName; + } else { + userName = QString("%1: %2") + .arg(pluginName) + .arg(outputs[j].name.c_str()); + friendlyName = outputs[j].name.c_str(); + } + + bool configurable = (!plugin->getPrograms().empty() || + !plugin->getParameterDescriptors().empty()); + +// std::cerr << "Feature extraction plugin transform: " << transformId.toStdString() << std::endl; + + transforms[transformId] = + TransformDesc(tr("Analysis"), + category, + transformId, + userName, + friendlyName, + description, + maker, + units, + configurable); + } + + delete plugin; + } +} + +void +TransformFactory::populateRealTimePlugins(TransformDescriptionMap &transforms) +{ + std::vector plugs = + RealTimePluginFactory::getAllPluginIdentifiers(); + + static 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 << " control output ports, " << descriptor->audioOutputPortCount << " audio outputs, " << descriptor->audioInputPortCount << " audio inputs" << std::endl; + + QString pluginName = descriptor->name.c_str(); + QString category = factory->getPluginCategory(pluginId); + bool configurable = (descriptor->parameterCount > 0); + QString maker = descriptor->maker.c_str(); + if (maker == "") maker = tr(""); + + if (descriptor->audioInputPortCount > 0) { + + for (size_t j = 0; j < descriptor->controlOutputPortCount; ++j) { + + QString transformId = QString("%1:%2").arg(pluginId).arg(j); + QString userName; + QString units; + QString portName; + + if (j < descriptor->controlOutputPortNames.size() && + descriptor->controlOutputPortNames[j] != "") { + + portName = descriptor->controlOutputPortNames[j].c_str(); + + userName = tr("%1: %2") + .arg(pluginName) + .arg(portName); + + if (unitRE.indexIn(portName) >= 0) { + units = unitRE.cap(1); + } + + } else if (descriptor->controlOutputPortCount > 1) { + + userName = tr("%1: Output %2") + .arg(pluginName) + .arg(j + 1); + + } else { + + userName = pluginName; + } + + QString description; + + if (portName != "") { + description = tr("Extract \"%1\" data output from \"%2\" effect plugin (from %3)") + .arg(portName) + .arg(pluginName) + .arg(maker); + } else { + description = tr("Extract data output %1 from \"%2\" effect plugin (from %3)") + .arg(j + 1) + .arg(pluginName) + .arg(maker); + } + + transforms[transformId] = + TransformDesc(tr("Effects Data"), + category, + transformId, + userName, + userName, + description, + maker, + units, + configurable); + } + } + + if (!descriptor->isSynth || descriptor->audioInputPortCount > 0) { + + if (descriptor->audioOutputPortCount > 0) { + + QString transformId = QString("%1:A").arg(pluginId); + QString type = tr("Effects"); + + QString description = tr("Transform audio signal with \"%1\" effect plugin (from %2)") + .arg(pluginName) + .arg(maker); + + if (descriptor->audioInputPortCount == 0) { + type = tr("Generators"); + QString description = tr("Generate audio signal using \"%1\" plugin (from %2)") + .arg(pluginName) + .arg(maker); + } + + transforms[transformId] = + TransformDesc(type, + category, + transformId, + pluginName, + pluginName, + description, + maker, + "", + configurable); + } + } + } +} + +QString +TransformFactory::getTransformName(TransformId identifier) +{ + if (m_transforms.find(identifier) != m_transforms.end()) { + return m_transforms[identifier].name; + } else return ""; +} + +QString +TransformFactory::getTransformFriendlyName(TransformId identifier) +{ + if (m_transforms.find(identifier) != m_transforms.end()) { + return m_transforms[identifier].friendlyName; + } else return ""; +} + +QString +TransformFactory::getTransformUnits(TransformId identifier) +{ + if (m_transforms.find(identifier) != m_transforms.end()) { + return m_transforms[identifier].units; + } else return ""; +} + +bool +TransformFactory::isTransformConfigurable(TransformId identifier) +{ + if (m_transforms.find(identifier) != m_transforms.end()) { + return m_transforms[identifier].configurable; + } else return false; +} + +bool +TransformFactory::getTransformChannelRange(TransformId identifier, + int &min, int &max) +{ + QString id = identifier.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(TransformId identifier, Vamp::PluginBase *plugin, + int &minChannels, int &maxChannels) +{ + Vamp::Plugin *vp = 0; + if ((vp = dynamic_cast(plugin)) || + (vp = dynamic_cast(plugin))) { + minChannels = vp->getMinChannelCount(); + maxChannels = vp->getMaxChannelCount(); + return true; + } else { + return getTransformChannelRange(identifier, minChannels, maxChannels); + } +} + +Model * +TransformFactory::getConfigurationForTransform(TransformId identifier, + const std::vector &candidateInputModels, + PluginTransform::ExecutionContext &context, + QString &configurationXml, + AudioCallbackPlaySource *source) +{ + if (candidateInputModels.empty()) return 0; + + //!!! This will need revision -- we'll have to have a callback + //from the dialog for when the candidate input model is changed, + //as we'll need to reinitialise the channel settings in the dialog + Model *inputModel = candidateInputModels[0]; //!!! for now + QStringList candidateModelNames; + std::map modelMap; + for (size_t i = 0; i < candidateInputModels.size(); ++i) { + QString modelName = candidateInputModels[i]->objectName(); + QString origModelName = modelName; + int dupcount = 1; + while (modelMap.find(modelName) != modelMap.end()) { + modelName = tr("%1 <%2>").arg(origModelName).arg(++dupcount); + } + modelMap[modelName] = candidateInputModels[i]; + candidateModelNames.push_back(modelName); + } + + QString id = identifier.section(':', 0, 2); + QString output = identifier.section(':', 3); + QString outputLabel = ""; + QString outputDescription = ""; + + bool ok = false; + configurationXml = m_lastConfigurations[identifier]; + +// std::cerr << "last configuration: " << configurationXml.toStdString() << std::endl; + + Vamp::PluginBase *plugin = 0; + + bool frequency = false; + bool effect = false; + bool generator = false; + + if (FeatureExtractionPluginFactory::instanceFor(id)) { + + std::cerr << "getConfigurationForTransform: instantiating Vamp plugin" << std::endl; + + Vamp::Plugin *vp = + FeatureExtractionPluginFactory::instanceFor(id)->instantiatePlugin + (id, inputModel->getSampleRate()); + + if (vp) { + + plugin = vp; + frequency = (vp->getInputDomain() == Vamp::Plugin::FrequencyDomain); + + std::vector od = + vp->getOutputDescriptors(); + if (od.size() > 1) { + for (size_t i = 0; i < od.size(); ++i) { + if (od[i].identifier == output.toStdString()) { + outputLabel = od[i].name.c_str(); + outputDescription = od[i].description.c_str(); + break; + } + } + } + } + + } else if (RealTimePluginFactory::instanceFor(id)) { + + RealTimePluginFactory *factory = RealTimePluginFactory::instanceFor(id); + const RealTimePluginDescriptor *desc = factory->getPluginDescriptor(id); + + if (desc->audioInputPortCount > 0 && + desc->audioOutputPortCount > 0 && + !desc->isSynth) { + effect = true; + } + + if (desc->audioInputPortCount == 0) { + generator = true; + } + + if (output != "A") { + int outputNo = output.toInt(); + if (outputNo >= 0 && outputNo < int(desc->controlOutputPortCount)) { + outputLabel = desc->controlOutputPortNames[outputNo].c_str(); + } + } + + size_t sampleRate = inputModel->getSampleRate(); + size_t blockSize = 1024; + size_t channels = 1; + if (effect && source) { + sampleRate = source->getTargetSampleRate(); + blockSize = source->getTargetBlockSize(); + channels = source->getTargetChannelCount(); + } + + RealTimePluginInstance *rtp = factory->instantiatePlugin + (id, 0, 0, sampleRate, blockSize, channels); + + plugin = rtp; + + if (effect && source && rtp) { + source->setAuditioningPlugin(rtp); + } + } + + if (plugin) { + + context = PluginTransform::ExecutionContext(context.channel, 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(identifier, plugin, minChannels, maxChannels); + + int targetChannels = sourceChannels; + if (!effect) { + if (sourceChannels < minChannels) targetChannels = minChannels; + if (sourceChannels > maxChannels) targetChannels = maxChannels; + } + + int defaultChannel = context.channel; + + PluginParameterDialog *dialog = new PluginParameterDialog(plugin); + + if (candidateModelNames.size() > 1 && !generator) { + dialog->setCandidateInputModels(candidateModelNames); + } + + if (targetChannels > 0) { + dialog->setChannelArrangement(sourceChannels, targetChannels, + defaultChannel); + } + + dialog->setOutputLabel(outputLabel, outputDescription); + + dialog->setShowProcessingOptions(true, frequency); + + if (dialog->exec() == QDialog::Accepted) { + ok = true; + } + + QString selectedInput = dialog->getInputModel(); + if (selectedInput != "") { + if (modelMap.find(selectedInput) != modelMap.end()) { + inputModel = modelMap[selectedInput]; + std::cerr << "Found selected input \"" << selectedInput.toStdString() << "\" in model map, result is " << inputModel << std::endl; + } else { + std::cerr << "Failed to find selected input \"" << selectedInput.toStdString() << "\" in model map" << std::endl; + } + } else { + std::cerr << "Selected input empty: \"" << selectedInput.toStdString() << "\"" << std::endl; + } + + configurationXml = PluginXml(plugin).toXmlString(); + context.channel = dialog->getChannel(); + + dialog->getProcessingParameters(context.stepSize, + context.blockSize, + context.windowType); + + context.makeConsistentWithPlugin(plugin); + + delete dialog; + + if (effect && source) { + source->setAuditioningPlugin(0); // will delete our plugin + } else { + delete plugin; + } + } + + if (ok) m_lastConfigurations[identifier] = configurationXml; + + return ok ? inputModel : 0; +} + +PluginTransform::ExecutionContext +TransformFactory::getDefaultContextForTransform(TransformId identifier, + Model *inputModel) +{ + PluginTransform::ExecutionContext context(-1); + + QString id = identifier.section(':', 0, 2); + + if (FeatureExtractionPluginFactory::instanceFor(id)) { + + Vamp::Plugin *vp = + FeatureExtractionPluginFactory::instanceFor(id)->instantiatePlugin + (id, inputModel ? inputModel->getSampleRate() : 48000); + + if (vp) { + context = PluginTransform::ExecutionContext(-1, vp); + delete vp; + } + } + + return context; +} + +Transform * +TransformFactory::createTransform(TransformId identifier, Model *inputModel, + const PluginTransform::ExecutionContext &context, + QString configurationXml) +{ + Transform *transform = 0; + + QString id = identifier.section(':', 0, 2); + QString output = identifier.section(':', 3); + + if (FeatureExtractionPluginFactory::instanceFor(id)) { + transform = new FeatureExtractionPluginTransform(inputModel, + id, + context, + configurationXml, + output); + } else if (RealTimePluginFactory::instanceFor(id)) { + transform = new RealTimePluginTransform(inputModel, + id, + context, + configurationXml, + getTransformUnits(identifier), + output == "A" ? -1 : + output.toInt()); + } else { + std::cerr << "TransformFactory::createTransform: Unknown transform \"" + << identifier.toStdString() << "\"" << std::endl; + return transform; + } + + if (transform) transform->setObjectName(identifier); + return transform; +} + +Model * +TransformFactory::transform(TransformId identifier, Model *inputModel, + const PluginTransform::ExecutionContext &context, + QString configurationXml) +{ + Transform *t = createTransform(identifier, inputModel, context, + configurationXml); + + if (!t) return 0; + + connect(t, SIGNAL(finished()), this, SLOT(transformFinished())); + + m_runningTransforms.insert(t); + + t->start(); + Model *model = t->detachOutputModel(); + + if (model) { + QString imn = inputModel->objectName(); + QString trn = getTransformFriendlyName(identifier); + if (imn != "") { + if (trn != "") { + model->setObjectName(tr("%1: %2").arg(imn).arg(trn)); + } else { + model->setObjectName(imn); + } + } else if (trn != "") { + model->setObjectName(trn); + } + } else { + t->wait(); + } + + return model; +} + +void +TransformFactory::transformFinished() +{ + QObject *s = sender(); + Transform *transform = dynamic_cast(s); + + std::cerr << "TransformFactory::transformFinished(" << transform << ")" << std::endl; + + if (!transform) { + std::cerr << "WARNING: TransformFactory::transformFinished: sender is not a transform" << std::endl; + return; + } + + if (m_runningTransforms.find(transform) == m_runningTransforms.end()) { + std::cerr << "WARNING: TransformFactory::transformFinished(" + << transform + << "): I have no record of this transform running!" + << std::endl; + } + + m_runningTransforms.erase(transform); + + transform->wait(); // unnecessary but reassuring + delete transform; +} + +void +TransformFactory::modelAboutToBeDeleted(Model *m) +{ + TransformSet affected; + + for (TransformSet::iterator i = m_runningTransforms.begin(); + i != m_runningTransforms.end(); ++i) { + + Transform *t = *i; + + if (t->getInputModel() == m || t->getOutputModel() == m) { + affected.insert(t); + } + } + + for (TransformSet::iterator i = affected.begin(); + i != affected.end(); ++i) { + + Transform *t = *i; + + t->abandon(); + + t->wait(); // this should eventually call back on + // transformFinished, which will remove from + // m_runningTransforms and delete. + } +} + diff --git a/transform/TransformFactory.h b/transform/TransformFactory.h new file mode 100644 index 0000000..33eea14 --- /dev/null +++ b/transform/TransformFactory.h @@ -0,0 +1,183 @@ +/* -*- 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 and QMUL. + + 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 "PluginTransform.h" + +#include +#include + +namespace Vamp { class PluginBase; } + +class AudioCallbackPlaySource; + +class TransformFactory : public QObject +{ + Q_OBJECT + +public: + virtual ~TransformFactory(); + + static TransformFactory *getInstance(); + + // The identifier is intended to be computer-referenceable, and + // unique within the application. The name 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 name. The type is also intended to be + // user-readable, for use in menus. + + struct TransformDesc { + + TransformDesc() { } + TransformDesc(QString _type, QString _category, + TransformId _identifier, QString _name, + QString _friendlyName, QString _description, + QString _maker, QString _units, bool _configurable) : + type(_type), category(_category), + identifier(_identifier), name(_name), + friendlyName(_friendlyName), description(_description), + maker(_maker), units(_units), configurable(_configurable) { } + + QString type; // e.g. feature extraction plugin + QString category; // e.g. time > onsets + TransformId identifier; // e.g. vamp:vamp-aubio:aubioonset + QString name; // plugin's name if 1 output, else "name: output" + QString friendlyName; // short text for layer name + QString description; // sentence describing transform + QString maker; + QString units; + bool configurable; + + bool operator<(const TransformDesc &od) const { + return (name < od.name); + }; + }; + typedef std::vector TransformList; + + TransformList getAllTransforms(); + + std::vector getAllTransformTypes(); + + std::vector getTransformCategories(QString transformType); + std::vector getTransformMakers(QString transformType); + + /** + * Get a configuration XML string for the given transform (by + * asking the user, most likely). Returns the selected input + * model if the transform is acceptable, 0 if the operation should + * be cancelled. Audio callback play source may be used to + * audition effects plugins, if provided. + */ + Model *getConfigurationForTransform(TransformId identifier, + const std::vector &candidateInputModels, + PluginTransform::ExecutionContext &context, + QString &configurationXml, + AudioCallbackPlaySource *source = 0); + + /** + * Get the default execution context for the given transform + * and input model (if known). + */ + PluginTransform::ExecutionContext getDefaultContextForTransform(TransformId identifier, + Model *inputModel = 0); + + /** + * 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(TransformId identifier, Model *inputModel, + const PluginTransform::ExecutionContext &context, + QString configurationXml = ""); + + /** + * Full name of a transform, suitable for putting on a menu. + */ + QString getTransformName(TransformId identifier); + + /** + * Brief but friendly name of a transform, suitable for use + * as the name of the output layer. + */ + QString getTransformFriendlyName(TransformId identifier); + + QString getTransformUnits(TransformId identifier); + + /** + * 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(TransformId identifier); + + /** + * 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. Return false if it doesn't care. + */ + bool getTransformChannelRange(TransformId identifier, + int &minChannels, int &maxChannels); + +protected slots: + void transformFinished(); + + void modelAboutToBeDeleted(Model *); + +protected: + Transform *createTransform(TransformId identifier, Model *inputModel, + const PluginTransform::ExecutionContext &context, + QString configurationXml); + + struct TransformIdent + { + TransformId identifier; + QString configurationXml; + }; + + typedef std::map TransformConfigurationMap; + TransformConfigurationMap m_lastConfigurations; + + typedef std::map TransformDescriptionMap; + TransformDescriptionMap m_transforms; + + typedef std::set TransformSet; + TransformSet m_runningTransforms; + + void populateTransforms(); + void populateFeatureExtractionPlugins(TransformDescriptionMap &); + void populateRealTimePlugins(TransformDescriptionMap &); + + bool getChannelRange(TransformId identifier, + Vamp::PluginBase *plugin, int &min, int &max); + + static TransformFactory *m_instance; +}; + + +#endif diff --git a/vect.pro b/vect.pro new file mode 100644 index 0000000..ea14c93 --- /dev/null +++ b/vect.pro @@ -0,0 +1,77 @@ + +TEMPLATE = app + +SV_UNIT_PACKAGES = vamp vamp-hostsdk fftw3f samplerate jack portaudio mad id3tag oggz fishsound lrdf raptor sndfile liblo +load(../sonic-visualiser/sv.prf) + +CONFIG += sv qt thread warn_on stl rtti exceptions +QT += xml network + +TARGET = vect + +ICON = icons/sv-macicon.icns + +DEPENDPATH += . ../sonic-visualiser audioio document i18n main osc transform +INCLUDEPATH += . ../sonic-visualiser audioio document transform osc main +LIBPATH = ../sonic-visualiser/view ../sonic-visualiser/layer ../sonic-visualiser/data ../sonic-visualiser/widgets ../sonic-visualiser/plugin ../sonic-visualiser/base ../sonic-visualiser/system $$LIBPATH + +contains(DEFINES, BUILD_STATIC):LIBS -= -ljack + +LIBS = -lsvview -lsvlayer -lsvdata -lsvwidgets -lsvplugin -lsvbase -lsvsystem $$LIBS + +PRE_TARGETDEPS += ../sonic-visualiser/view/libsvview.a \ + ../sonic-visualiser/layer/libsvlayer.a \ + ../sonic-visualiser/data/libsvdata.a \ + ../sonic-visualiser/widgets/libsvwidgets.a \ + ../sonic-visualiser/plugin/libsvplugin.a \ + ../sonic-visualiser/base/libsvbase.a \ + ../sonic-visualiser/system/libsvsystem.a + +OBJECTS_DIR = tmp_obj +MOC_DIR = tmp_moc + +# Input +HEADERS += audioio/AudioCallbackPlaySource.h \ + audioio/AudioCallbackPlayTarget.h \ + audioio/AudioCoreAudioTarget.h \ + audioio/AudioGenerator.h \ + audioio/AudioJACKTarget.h \ + audioio/AudioPortAudioTarget.h \ + audioio/AudioTargetFactory.h \ + audioio/PhaseVocoderTimeStretcher.h \ + audioio/PlaySpeedRangeMapper.h \ + document/Document.h \ + document/SVFileReader.h \ + main/MainWindow.h \ + main/PreferencesDialog.h \ + osc/OSCMessage.h \ + osc/OSCQueue.h \ + transform/FeatureExtractionPluginTransform.h \ + transform/PluginTransform.h \ + transform/RealTimePluginTransform.h \ + transform/Transform.h \ + transform/TransformFactory.h +SOURCES += audioio/AudioCallbackPlaySource.cpp \ + audioio/AudioCallbackPlayTarget.cpp \ + audioio/AudioCoreAudioTarget.cpp \ + audioio/AudioGenerator.cpp \ + audioio/AudioJACKTarget.cpp \ + audioio/AudioPortAudioTarget.cpp \ + audioio/AudioTargetFactory.cpp \ + audioio/PhaseVocoderTimeStretcher.cpp \ + audioio/PlaySpeedRangeMapper.cpp \ + document/Document.cpp \ + document/SVFileReader.cpp \ + main/main.cpp \ + main/MainWindow.cpp \ + main/PreferencesDialog.cpp \ + osc/OSCMessage.cpp \ + osc/OSCQueue.cpp \ + transform/FeatureExtractionPluginTransform.cpp \ + transform/PluginTransform.cpp \ + transform/RealTimePluginTransform.cpp \ + transform/Transform.cpp \ + transform/TransformFactory.cpp +RESOURCES += vect.qrc + + diff --git a/vect.qrc b/vect.qrc new file mode 100644 index 0000000..26704a7 --- /dev/null +++ b/vect.qrc @@ -0,0 +1,90 @@ + + + icons/waveform.png + icons/spectrum.png + icons/spectrogram.png + icons/timeruler.png + icons/pane.png + icons/instants.png + icons/notes.png + icons/values.png + icons/colour3d.png + icons/playpause.png + icons/ffwd.png + icons/ffwd-end.png + icons/rewind.png + icons/rewind-start.png + icons/playselection.png + icons/playloop.png + icons/solo.png + icons/fader_background.png + icons/fader_knob.png + icons/fader_knob_red.png + icons/fader_leds.png + icons/faders.png + icons/select.png + icons/text.png + icons/draw.png + icons/draw-curve.png + icons/measure.png + icons/measure1cursor.xbm + icons/measure1mask.xbm + icons/measure2cursor.xbm + icons/measure2mask.xbm + icons/move.png + icons/navigate.png + icons/zoom.png + icons/zoom-in.png + icons/zoom-out.png + icons/zoom-fit.png + icons/undo.png + icons/redo.png + icons/new.png + icons/exit.png + icons/speaker.png + icons/annotation.png + icons/fileopen.png + icons/fileopensession.png + icons/fileopenaudio.png + icons/fileopen-22.png + icons/fileclose.png + icons/filenew.png + icons/filenew-22.png + icons/filesave.png + icons/filesave-22.png + icons/filesaveas.png + icons/filesaveas-22.png + icons/editdelete.png + icons/editcut.png + icons/editcopy.png + icons/editpaste.png + icons/mono.png + icons/stereo.png + icons/sharpen.png + icons/help.png + icons/sv-16x16.png + icons/sv-22x22.png + icons/sv-24x24.png + icons/sv-32x32.png + icons/sv-48x48.png + icons/sv-64x64.png + icons/sv-128x128.png + samples/bass.wav + samples/beep.wav + samples/bounce.wav + samples/clap.wav + samples/click.wav + samples/cowbell.wav + samples/hihat.wav + samples/kick.wav + samples/organ.wav + samples/piano.wav + samples/snare.wav + samples/stick.wav + samples/strike.wav + samples/tap.wav + i18n/sonic-visualiser_ru.qm + i18n/sonic-visualiser_en_GB.qm + i18n/tips_en.xml + + diff --git a/version.h b/version.h new file mode 100644 index 0000000..560f347 --- /dev/null +++ b/version.h @@ -0,0 +1 @@ +#define VECT_VERSION "[vect] 0.1"