/* Copyright 2016, Ableton AG, Berlin. All rights reserved. * * 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. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * If you would like to incorporate Link into a proprietary software application, * please contact . */ #include "AudioEngine.hpp" // Make sure to define this before is included for Windows #ifdef LINK_PLATFORM_WINDOWS #define _USE_MATH_DEFINES #endif #include #include namespace ableton { namespace linkaudio { AudioEngine::AudioEngine(Link& link) : mLink(link) , mSampleRate(44100.) , mOutputLatency(std::chrono::microseconds{0}) , mSharedEngineData({0., false, false, 4., false}) , mLockfreeEngineData(mSharedEngineData) , mTimeAtLastClick{} , mIsPlaying(false) { if (!mOutputLatency.is_lock_free()) { std::cout << "WARNING: AudioEngine::mOutputLatency is not lock free!" << std::endl; } } void AudioEngine::startPlaying() { std::lock_guard lock(mEngineDataGuard); mSharedEngineData.requestStart = true; } void AudioEngine::stopPlaying() { std::lock_guard lock(mEngineDataGuard); mSharedEngineData.requestStop = true; } bool AudioEngine::isPlaying() const { return mLink.captureAppSessionState().isPlaying(); } double AudioEngine::beatTime() const { const auto sessionState = mLink.captureAppSessionState(); return sessionState.beatAtTime(mLink.clock().micros(), mSharedEngineData.quantum); } void AudioEngine::setTempo(double tempo) { std::lock_guard lock(mEngineDataGuard); mSharedEngineData.requestedTempo = tempo; } double AudioEngine::quantum() const { return mSharedEngineData.quantum; } void AudioEngine::setQuantum(double quantum) { std::lock_guard lock(mEngineDataGuard); mSharedEngineData.quantum = quantum; } bool AudioEngine::isStartStopSyncEnabled() const { return mLink.isStartStopSyncEnabled(); } void AudioEngine::setStartStopSyncEnabled(const bool enabled) { mLink.enableStartStopSync(enabled); } void AudioEngine::setBufferSize(std::size_t size) { mBuffer = std::vector(size, 0.); } void AudioEngine::setSampleRate(double sampleRate) { mSampleRate = sampleRate; } AudioEngine::EngineData AudioEngine::pullEngineData() { auto engineData = EngineData{}; if (mEngineDataGuard.try_lock()) { engineData.requestedTempo = mSharedEngineData.requestedTempo; mSharedEngineData.requestedTempo = 0; engineData.requestStart = mSharedEngineData.requestStart; mSharedEngineData.requestStart = false; engineData.requestStop = mSharedEngineData.requestStop; mSharedEngineData.requestStop = false; mLockfreeEngineData.quantum = mSharedEngineData.quantum; mLockfreeEngineData.startStopSyncOn = mSharedEngineData.startStopSyncOn; mEngineDataGuard.unlock(); } engineData.quantum = mLockfreeEngineData.quantum; return engineData; } void AudioEngine::renderMetronomeIntoBuffer(const Link::SessionState sessionState, const double quantum, const std::chrono::microseconds beginHostTime, const std::size_t numSamples) { using namespace std::chrono; // Metronome frequencies static const double highTone = 1567.98; static const double lowTone = 1108.73; // 100ms click duration static const auto clickDuration = duration{0.1}; // The number of microseconds that elapse between samples const auto microsPerSample = 1e6 / mSampleRate; for (std::size_t i = 0; i < numSamples; ++i) { double amplitude = 0.; // Compute the host time for this sample and the last. const auto hostTime = beginHostTime + microseconds(llround(static_cast(i) * microsPerSample)); const auto lastSampleHostTime = hostTime - microseconds(llround(microsPerSample)); // Only make sound for positive beat magnitudes. Negative beat // magnitudes are count-in beats. if (sessionState.beatAtTime(hostTime, quantum) >= 0.) { // If the phase wraps around between the last sample and the // current one with respect to a 1 beat quantum, then a click // should occur. if (sessionState.phaseAtTime(hostTime, 1) < sessionState.phaseAtTime(lastSampleHostTime, 1)) { mTimeAtLastClick = hostTime; } const auto secondsAfterClick = duration_cast>(hostTime - mTimeAtLastClick); // If we're within the click duration of the last beat, render // the click tone into this sample if (secondsAfterClick < clickDuration) { // If the phase of the last beat with respect to the current // quantum was zero, then it was at a quantum boundary and we // want to use the high tone. For other beats within the // quantum, use the low tone. const auto freq = floor(sessionState.phaseAtTime(hostTime, quantum)) == 0 ? highTone : lowTone; // Simple cosine synth amplitude = cos(2 * M_PI * secondsAfterClick.count() * freq) * (1 - sin(5 * M_PI * secondsAfterClick.count())); } } mBuffer[i] = amplitude; } } void AudioEngine::audioCallback( const std::chrono::microseconds hostTime, const std::size_t numSamples) { const auto engineData = pullEngineData(); auto sessionState = mLink.captureAudioSessionState(); // Clear the buffer std::fill(mBuffer.begin(), mBuffer.end(), 0); if (engineData.requestStart) { sessionState.setIsPlaying(true, hostTime); } if (engineData.requestStop) { sessionState.setIsPlaying(false, hostTime); } if (!mIsPlaying && sessionState.isPlaying()) { // Reset the timeline so that beat 0 corresponds to the time when transport starts sessionState.requestBeatAtStartPlayingTime(0, engineData.quantum); mIsPlaying = true; } else if (mIsPlaying && !sessionState.isPlaying()) { mIsPlaying = false; } if (engineData.requestedTempo > 0) { // Set the newly requested tempo from the beginning of this buffer sessionState.setTempo(engineData.requestedTempo, hostTime); } // Timeline modifications are complete, commit the results mLink.commitAudioSessionState(sessionState); if (mIsPlaying) { // As long as the engine is playing, generate metronome clicks in // the buffer at the appropriate beats. renderMetronomeIntoBuffer(sessionState, engineData.quantum, hostTime, numSamples); } } } // namespace linkaudio } // namespace ableton