diff --git a/client/Makefile b/client/Makefile index f06063e0..1eaffc30 100644 --- a/client/Makefile +++ b/client/Makefile @@ -43,7 +43,7 @@ ifneq ($(SANITIZE), ) endif CXXFLAGS += $(ADD_CFLAGS) -std=c++14 -Wall -Wextra -Wpedantic -Wno-unused-function -DBOOST_ERROR_CODE_HEADER_ONLY -DHAS_FLAC -DHAS_OGG -DHAS_OPUS -DVERSION=\"$(VERSION)\" -I. -I.. -I../common -LDFLAGS += $(ADD_LDFLAGS) -logg -lFLAC -lopus +LDFLAGS += $(ADD_LDFLAGS) -logg -lFLAC -lopus -lsoxr OBJ = snapclient.o stream.o client_connection.o time_provider.o player/player.o decoder/pcm_decoder.o decoder/ogg_decoder.o decoder/flac_decoder.o decoder/opus_decoder.o controller.o ../common/sample_format.o diff --git a/client/client_settings.hpp b/client/client_settings.hpp index 84200736..0fca36eb 100644 --- a/client/client_settings.hpp +++ b/client/client_settings.hpp @@ -38,6 +38,7 @@ struct ClientSettings std::string player_name{""}; int latency{0}; PcmDevice pcm_device; + SampleFormat sample_format; }; struct LoggingSettings diff --git a/client/controller.cpp b/client/controller.cpp index 4a9d51a3..b21d5924 100644 --- a/client/controller.cpp +++ b/client/controller.cpp @@ -121,7 +121,7 @@ void Controller::onMessageReceived(ClientConnection* /*connection*/, const msg:: sampleFormat_ = decoder_->setHeader(headerChunk_.get()); LOG(NOTICE) << TAG("state") << "sampleformat: " << sampleFormat_.rate << ":" << sampleFormat_.bits << ":" << sampleFormat_.channels << "\n"; - stream_ = make_shared(sampleFormat_); + stream_ = make_shared(sampleFormat_, settings_.player.sample_format); stream_->setBufferLen(serverSettings_->getBufferMs() - settings_.player.latency); const auto& pcm_device = settings_.player.pcm_device; diff --git a/client/snapclient.cpp b/client/snapclient.cpp index 09bf7064..3205f012 100644 --- a/client/snapclient.cpp +++ b/client/snapclient.cpp @@ -84,7 +84,6 @@ int main(int argc, char** argv) ClientSettings settings; string pcm_device("default"); - OptionParser op("Allowed options"); auto helpSwitch = op.add("", "help", "produce help message"); auto groffSwitch = op.add("", "groff", "produce groff message"); @@ -106,6 +105,7 @@ int main(int argc, char** argv) /*auto instanceValue =*/op.add>("i", "instance", "instance id", 1, &settings.instance); /*auto hostIdValue =*/op.add>("", "hostID", "unique host id", "", &settings.host_id); op.add>("", "player", "audio backend", "", &settings.player.player_name); + auto sample_format = op.add>("", "sampleformat", "resample audio stream to sampleformat", ""); try { @@ -210,6 +210,11 @@ int main(int argc, char** argv) } #endif + if (sample_format->is_set()) + { + settings.player.sample_format = SampleFormat(sample_format->value()); + } + bool active = true; std::shared_ptr controller; auto signal_handler = install_signal_handler({SIGHUP, SIGTERM, SIGINT}, diff --git a/client/stream.cpp b/client/stream.cpp index 52057d41..9892dd32 100644 --- a/client/stream.cpp +++ b/client/stream.cpp @@ -28,17 +28,19 @@ using namespace std; namespace cs = chronos; -Stream::Stream(const SampleFormat& sampleFormat) - : format_(sampleFormat), sleep_(0), median_(0), shortMedian_(0), lastUpdate_(0), playedFrames_(0), bufferMs_(cs::msec(500)) +Stream::Stream(const SampleFormat& in_format, const SampleFormat& out_format) + : in_format_(in_format), sleep_(0), median_(0), shortMedian_(0), lastUpdate_(0), playedFrames_(0), bufferMs_(cs::msec(500)), soxr_(nullptr), frame_delta_(0) { buffer_.setSize(500); shortBuffer_.setSize(100); miniBuffer_.setSize(20); // cardBuffer_.setSize(50); - // input_rate_ = format_.rate; - // format_.rate = 48000; - // output_rate_ = static_cast(format_.rate); + if (out_format.rate != 0) + format_ = out_format; + else + format_ = in_format_; + /* 48000 x ------- = ----- @@ -46,16 +48,37 @@ Stream::Stream(const SampleFormat& sampleFormat) x = 1,000016667 / (1,000016667 - 1) */ - setRealSampleRate(format_.rate); + // setRealSampleRate(format_.rate); - // soxr_error_t error; - // soxr_io_spec_t iospec = soxr_io_spec(SOXR_INT16_I, SOXR_INT16_I); - // soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_HQ, 0); - // soxr_ = soxr_create(static_cast(input_rate_), static_cast(format_.rate), format_.channels, &error, &iospec, &q_spec, NULL); - // if (error) - // { - // LOG(ERROR) << "Error soxr_create: " << error << "\n"; - // } + if ((format_.rate != in_format_.rate) || (format_.bits != in_format_.bits)) + { + LOG(INFO) << "Resampling from " << in_format_.getFormat() << " to " << format_.getFormat() << "\n"; + soxr_error_t error; + + soxr_datatype_t in_type = SOXR_INT16_I; + soxr_datatype_t out_type = SOXR_INT16_I; + if (in_format_.sampleSize > 2) + in_type = SOXR_INT32_I; + if (format_.sampleSize > 2) + out_type = SOXR_INT32_I; + soxr_io_spec_t iospec = soxr_io_spec(in_type, out_type); + // HQ should be fine: http://sox.sourceforge.net/Docs/FAQ + soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_HQ, 0); + soxr_ = soxr_create(static_cast(in_format_.rate), static_cast(format_.rate), format_.channels, &error, &iospec, &q_spec, NULL); + if (error) + { + LOG(ERROR) << "Error soxr_create: " << error << "\n"; + } + // initialize the buffer with 20ms (~latency of the reampler) + resample_buffer_.resize(format_.frameSize * ceil(format_.msRate()) * 20); + } +} + + +Stream::~Stream() +{ + if (soxr_) + soxr_delete(soxr_); } @@ -75,7 +98,6 @@ void Stream::setBufferLen(size_t bufferLenMs) } - void Stream::clearChunks() { while (chunks_.size() > 0) @@ -89,41 +111,85 @@ void Stream::addChunk(unique_ptr chunk) while (chunks_.size() * chunk->duration().count() > 10000) chunks_.pop(); - chunks_.push(move(chunk)); - // LOG(DEBUG) << "new chunk: " << chunk->duration().count() << ", Chunks: " << chunks_.size() << "\n"; + // chunks_.push(move(chunk)); + // LOG(DEBUG) << "new chunk: " << chunk->duration().count() << ", Chunks: " << chunks_.size() << "\n"; - // if (std::abs(input_rate_ - output_rate_) <= 0.0000001) - // { - // chunks_.push(shared_ptr(chunk)); - // } - // else - // { - // size_t idone; - // size_t odone; - // auto out = new msg::PcmChunk(format_, 0); - // out->timestamp = chunk->timestamp; - // out->payloadSize = ceil(chunk->payloadSize * static_cast(output_rate_) / static_cast(input_rate_)); - // out->payload = (char*)malloc(out->payloadSize); + if (soxr_ == nullptr) + { + chunks_.push(move(chunk)); + } + else + { + size_t idone; + size_t odone; - // soxr_io_spec_t iospec = soxr_io_spec(SOXR_INT16_I, SOXR_INT16_I); - // soxr_quality_spec_t q_spec = soxr_quality_spec(SOXR_HQ, 0); - // // auto error = soxr_oneshot(static_cast(input_rate_), output_rate_, format_.channels, chunk->payload, chunk->getFrameCount(), &idone, - // // out->payload, out->payloadSize, &odone, &iospec, &q_spec, nullptr); - // auto error = soxr_process(soxr_, chunk->payload, chunk->getFrameCount(), &idone, out->payload, out->getFrameCount(), &odone); - // if (error) - // { - // LOG(ERROR) << "Error soxr_process: " << error << "\n"; - // delete out; - // } - // else - // { - // out->payloadSize = odone * out->format.frameSize; - // LOG(TRACE) << "Resample idone: " << idone << "/" << chunk->getFrameCount() << ", odone: " << odone << "/" - // << out->payloadSize / out->format.frameSize << "\n"; - // chunks_.push(shared_ptr(out)); - // } - // delete chunk; - // } + if (in_format_.bits == 24) + { + // sox expects 32 bit input, shift 8 bits left + int32_t* frames = (int32_t*)chunk->payload; + for (size_t n = 0; n < chunk->getSampleCount(); ++n) + frames[n] = frames[n] << 8; + } + + auto resample_buffer_framesize = resample_buffer_.size() / format_.frameSize; + auto error = soxr_process(soxr_, chunk->payload, chunk->getFrameCount(), &idone, resample_buffer_.data(), resample_buffer_framesize, &odone); + if (error) + { + LOG(ERROR) << "Error soxr_process: " << error << "\n"; + // delete out; + } + else + { + LOG(TRACE) << "Resample idone: " << idone << "/" << chunk->getFrameCount() << ", odone: " << odone << "/" + << resample_buffer_.size() / format_.frameSize << ", delay: " << soxr_delay(soxr_) << "\n"; + + // some data has been resampled (odone frames) and some is still in the pipe (soxr_delay frames) + if (odone > 0) + { + // get the resamples ts from the input ts + auto input_end_ts = chunk->start() + chunk->duration(); + double resampled_ms = (odone + soxr_delay(soxr_)) / format_.msRate(); + auto resampled_start = input_end_ts - std::chrono::microseconds(static_cast(resampled_ms * 1000.)); + + auto resampled_chunk = new msg::PcmChunk(format_, 0); + auto us = chrono::duration_cast(resampled_start.time_since_epoch()).count(); + resampled_chunk->timestamp.sec = us / 1000000; + resampled_chunk->timestamp.usec = us % 1000000; + + // copy from the resample_buffer to the resampled chunk + resampled_chunk->payloadSize = odone * format_.frameSize; + resampled_chunk->payload = (char*)realloc(resampled_chunk->payload, resampled_chunk->payloadSize); + memcpy(resampled_chunk->payload, resample_buffer_.data(), resampled_chunk->payloadSize); + + if (format_.bits == 24) + { + // sox has quantized to 32 bit, shift 8 bits right + int32_t* frames = (int32_t*)resampled_chunk->payload; + for (size_t n = 0; n < resampled_chunk->getSampleCount(); ++n) + { + // +128 to round to the nearest so that quantisation steps are distributed evenly + frames[n] = (frames[n] + 128) >> 8; + if (frames[n] > 0x7fffffff) + frames[n] = 0x7fffffff; + } + } + chunks_.push(shared_ptr(resampled_chunk)); + + // check if the resample_buffer is large enough, or if soxr was using all available space + if (odone == resample_buffer_framesize) + { + // buffer for resampled data too small, add space for 5ms + resample_buffer_.resize(resample_buffer_.size() + format_.frameSize * ceil(format_.msRate()) * 5); + LOG(INFO) << "Resample buffer completely filled, adding space for 5ms; new buffer size: " << resample_buffer_.size() << " bytes\n"; + } + + // //LOG(TRACE) << "ts: " << out->timestamp.sec << "s, " << out->timestamp.usec/1000.f << " ms, duration: " << odone / format_.msRate() << "\n"; + // int64_t next_us = us + static_cast(odone / format_.msRate() * 1000); + // LOG(TRACE) << "ts: " << us << ", next: " << next_us << ", diff: " << next_us_ - us << "\n"; + // next_us_ = next_us; + } + } + } } @@ -134,12 +200,12 @@ bool Stream::waitForChunk(size_t ms) const -cs::time_point_clk Stream::getSilentPlayerChunk(void* outputBuffer, unsigned long framesPerBuffer) +cs::time_point_clk Stream::getSilentPlayerChunk(void* outputBuffer, unsigned long frames) { if (!chunk_) chunk_ = chunks_.pop(); cs::time_point_clk tp = chunk_->start(); - memset(outputBuffer, 0, framesPerBuffer * format_.frameSize); + memset(outputBuffer, 0, frames * format_.frameSize); return tp; } @@ -182,16 +248,16 @@ time_point_clk Stream::seek(long ms) */ -cs::time_point_clk Stream::getNextPlayerChunk(void* outputBuffer, const cs::usec& timeout, unsigned long framesPerBuffer) +cs::time_point_clk Stream::getNextPlayerChunk(void* outputBuffer, const cs::usec& timeout, unsigned long frames) { if (!chunk_ && !chunks_.try_pop(chunk_, timeout)) throw 0; cs::time_point_clk tp = chunk_->start(); unsigned long read = 0; - while (read < framesPerBuffer) + while (read < frames) { - read += chunk_->readFrames(static_cast(outputBuffer) + read * format_.frameSize, framesPerBuffer - read); + read += chunk_->readFrames(static_cast(outputBuffer) + read * format_.frameSize, frames - read); if (chunk_->isEndOfChunk() && !chunks_.try_pop(chunk_, timeout)) throw 0; } @@ -199,22 +265,24 @@ cs::time_point_clk Stream::getNextPlayerChunk(void* outputBuffer, const cs::usec } -cs::time_point_clk Stream::getNextPlayerChunk(void* outputBuffer, const cs::usec& timeout, unsigned long framesPerBuffer, long framesCorrection) +cs::time_point_clk Stream::getNextPlayerChunk(void* outputBuffer, const cs::usec& timeout, unsigned long frames, long framesCorrection) { - if (framesCorrection < 0 && framesPerBuffer + framesCorrection <= 0) + if (framesCorrection < 0 && frames + framesCorrection <= 0) { // Avoid underflow in new char[] constructor. - framesCorrection = -framesPerBuffer + 1; + framesCorrection = -frames + 1; } - if (framesCorrection == 0) - return getNextPlayerChunk(outputBuffer, timeout, framesPerBuffer); + frame_delta_ -= framesCorrection; - long toRead = framesPerBuffer + framesCorrection; + if (framesCorrection == 0) + return getNextPlayerChunk(outputBuffer, timeout, frames); + + long toRead = frames + framesCorrection; char* buffer = new char[toRead * format_.frameSize]; cs::time_point_clk tp = getNextPlayerChunk(buffer, timeout, toRead); - const auto max = framesCorrection < 0 ? framesPerBuffer : toRead; + const auto max = framesCorrection < 0 ? frames : toRead; // Divide the buffer into one more slice than frames that need to be dropped. // We will drop/repeat 0 frames from the first slice, 1 frame from the second, ..., and framesCorrection frames from the last slice. size_t slices = abs(framesCorrection) + 1; @@ -228,42 +296,34 @@ cs::time_point_clk Stream::getNextPlayerChunk(void* outputBuffer, const cs::usec // Size of each slice. The last slice may be bigger. int size = max / slices; - // LOG(TRACE) << "getNextPlayerChunk, frames: " << framesPerBuffer << ", correction: " << framesCorrection << " (" << toRead << "), slices: " << slices + // LOG(TRACE) << "getNextPlayerChunk, frames: " << frames << ", correction: " << framesCorrection << " (" << toRead << "), slices: " << slices // << "\n"; size_t pos = 0; for (size_t n = 0; n < slices; ++n) { + // Adjust size in the last iteration, because the last slice may be bigger if (n + 1 == slices) - // Adjust size in the last iteration, because the last slice may be bigger size = max - pos; if (framesCorrection < 0) { // Read one frame less per slice from the input, but write a duplicated frame per slice to the output - - // LOG(TRACE) << "slice: " << n << ", size: " << size << ", out pos: " << pos << ", source pos: " << pos - n << "\n"; + // LOG(TRACE) << "duplicate - requested: " << frames << ", read: " << toRead << ", slice: " << n << ", size: " << size << ", out pos: " << pos << ", + // source pos: " << pos - n << "\n"; memcpy(static_cast(outputBuffer) + pos * format_.frameSize, buffer + (pos - n) * format_.frameSize, size * format_.frameSize); } else { // Read all input frames, but skip a frame per slice when writing to the output. - - // LOG(TRACE) << "slice: " << n << ", size: " << size << ", out pos: " << pos - n << ", source pos: " << pos << "\n"; + // LOG(TRACE) << "remove - requested: " << frames << ", read: " << toRead << ", slice: " << n << ", size: " << size << ", out pos: " << pos - n << + // ", source pos: " << pos << "\n"; memcpy(static_cast(outputBuffer) + (pos - n) * format_.frameSize, buffer + pos * format_.frameSize, size * format_.frameSize); } pos += size; } - // float idx = 0; - // for (size_t n = 0; n < framesPerBuffer; ++n) - // { - // size_t index(floor(idx)); // = (int)(ceil(n*factor)); - // memcpy((char*)outputBuffer + n * format_.frameSize, buffer + index * format_.frameSize, format_.frameSize); - // idx += factor; - // } delete[] buffer; - return tp; } @@ -312,7 +372,7 @@ void Stream::resetBuffers() } -bool Stream::getPlayerChunk(void* outputBuffer, const cs::usec& outputBufferDacTime, unsigned long framesPerBuffer) +bool Stream::getPlayerChunk(void* outputBuffer, const cs::usec& outputBufferDacTime, unsigned long frames) { if (outputBufferDacTime > bufferMs_) { @@ -328,7 +388,7 @@ bool Stream::getPlayerChunk(void* outputBuffer, const cs::usec& outputBufferDacT return false; } - playedFrames_ += framesPerBuffer; + playedFrames_ += frames; /// we have a chunk /// age = chunk age (server now - rec time: some positive value) - buffer (e.g. 1000ms) + time to DAC @@ -346,8 +406,8 @@ bool Stream::getPlayerChunk(void* outputBuffer, const cs::usec& outputBufferDacT try { - // LOG(DEBUG) << "framesPerBuffer: " << framesPerBuffer << "\tms: " << framesPerBuffer*2 / PLAYER_CHUNK_MS_SIZE << "\t" << PLAYER_CHUNK_SIZE << "\n"; - cs::nsec bufferDuration = cs::nsec(static_cast(framesPerBuffer / format_.nsRate())); + // LOG(DEBUG) << "frames: " << frames << "\tms: " << frames*2 / PLAYER_CHUNK_MS_SIZE << "\t" << PLAYER_CHUNK_SIZE << "\n"; + cs::nsec bufferDuration = cs::nsec(static_cast(frames / format_.nsRate())); // LOG(DEBUG) << "buffer duration: " << bufferDuration.count() << "\n"; cs::usec correction = cs::usec(0); @@ -358,8 +418,8 @@ bool Stream::getPlayerChunk(void* outputBuffer, const cs::usec& outputBufferDacT { LOG(INFO) << "sleep < -bufferDuration/2: " << cs::duration(sleep_) << " < " << -cs::duration(bufferDuration) / 2 << ", "; // We're early: not enough chunks_. play silence. Reference chunk_ is the oldest (front) one - sleep_ = chrono::duration_cast(TimeProvider::serverNow() - getSilentPlayerChunk(outputBuffer, framesPerBuffer) - bufferMs_ + - outputBufferDacTime); + sleep_ = + chrono::duration_cast(TimeProvider::serverNow() - getSilentPlayerChunk(outputBuffer, frames) - bufferMs_ + outputBufferDacTime); LOG(INFO) << "sleep: " << cs::duration(sleep_) << "\n"; if (sleep_ < -bufferDuration / 2) return true; @@ -413,9 +473,8 @@ bool Stream::getPlayerChunk(void* outputBuffer, const cs::usec& outputBufferDacT playedFrames_ = 0; //-= abs(correctAfterXFrames_); } - age = std::chrono::duration_cast(TimeProvider::serverNow() - - getNextPlayerChunk(outputBuffer, outputBufferDacTime, framesPerBuffer, framesCorrection) - bufferMs_ + - outputBufferDacTime); + age = std::chrono::duration_cast(TimeProvider::serverNow() - getNextPlayerChunk(outputBuffer, outputBufferDacTime, frames, framesCorrection) - + bufferMs_ + outputBufferDacTime); setRealSampleRate(format_.rate); if (sleep_.count() == 0) @@ -495,9 +554,10 @@ bool Stream::getPlayerChunk(void* outputBuffer, const cs::usec& outputBufferDacT median_ = buffer_.median(); shortMedian_ = shortBuffer_.median(); LOG(INFO) << "Chunk: " << age.count() / 100 << "\t" << miniBuffer_.median() / 100 << "\t" << shortMedian_ / 100 << "\t" << median_ / 100 << "\t" - << buffer_.size() << "\t" << cs::duration(outputBufferDacTime) << "\n"; + << buffer_.size() << "\t" << cs::duration(outputBufferDacTime) << "\t" << frame_delta_ << "\n"; // LOG(INFO) << "Chunk: " << age.count()/1000 << "\t" << miniBuffer_.median()/1000 << "\t" << shortMedian_/1000 << "\t" << median_/1000 << "\t" << // buffer_.size() << "\t" << cs::duration(outputBufferDacTime) << "\n"; + frame_delta_ = 0; } return (abs(cs::duration(age)) < 500); } diff --git a/client/stream.hpp b/client/stream.hpp index a33ec0e1..440e15e1 100644 --- a/client/stream.hpp +++ b/client/stream.hpp @@ -26,7 +26,7 @@ #include "message/pcm_chunk.hpp" #include #include -// #include +#include /// Time synchronized audio stream @@ -37,7 +37,8 @@ class Stream { public: - Stream(const SampleFormat& format); + Stream(const SampleFormat& in_format, const SampleFormat& out_format); + virtual ~Stream(); /// Adds PCM data to the queue void addChunk(std::unique_ptr chunk); @@ -58,9 +59,9 @@ public: bool waitForChunk(size_t ms) const; private: - chronos::time_point_clk getNextPlayerChunk(void* outputBuffer, const chronos::usec& timeout, unsigned long framesPerBuffer); - chronos::time_point_clk getNextPlayerChunk(void* outputBuffer, const chronos::usec& timeout, unsigned long framesPerBuffer, long framesCorrection); - chronos::time_point_clk getSilentPlayerChunk(void* outputBuffer, unsigned long framesPerBuffer); + chronos::time_point_clk getNextPlayerChunk(void* outputBuffer, const chronos::usec& timeout, unsigned long frames); + chronos::time_point_clk getNextPlayerChunk(void* outputBuffer, const chronos::usec& timeout, unsigned long frames, long framesCorrection); + chronos::time_point_clk getSilentPlayerChunk(void* outputBuffer, unsigned long frames); chronos::time_point_clk seek(long ms); // time_point_ms seekTo(const time_point_ms& to); void updateBuffers(int age); @@ -68,6 +69,7 @@ private: void setRealSampleRate(double sampleRate); SampleFormat format_; + SampleFormat in_format_; chronos::usec sleep_; @@ -84,9 +86,11 @@ private: unsigned long playedFrames_; long correctAfterXFrames_; chronos::msec bufferMs_; - // size_t input_rate_; - // double output_rate_; - // soxr_t soxr_; + + soxr_t soxr_; + std::vector resample_buffer_; + int frame_delta_; + // int64_t next_us_; }; diff --git a/common/sample_format.cpp b/common/sample_format.cpp index 089db759..c86615d9 100644 --- a/common/sample_format.cpp +++ b/common/sample_format.cpp @@ -30,7 +30,10 @@ using namespace std; -SampleFormat::SampleFormat() = default; +SampleFormat::SampleFormat() +{ + setFormat(0, 0, 0); +} SampleFormat::SampleFormat(const std::string& format) diff --git a/externals/Makefile b/externals/Makefile index 849a4eff..c7b36492 100644 --- a/externals/Makefile +++ b/externals/Makefile @@ -14,9 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -.PHONY: all check-env flac ogg opus tremor oboe +.PHONY: all check-env flac ogg opus tremor oboe soxr -all: flac ogg opus tremor oboe +all: flac ogg opus tremor oboe soxr check-env: # if [ ! -d "flac" ]; then \ @@ -128,3 +128,17 @@ oboe: check-env make clean; \ cd ..; \ rm -rf build; + +soxr: check-env + @cd /home/johannes/Develop/soxr; \ + export CC="$(CC)"; \ + export CXX="$(CXX)"; \ + export CPPFLAGS="$(CPPFLAGS)"; \ + mkdir build; \ + cd build; \ + cmake -DBUILD_SHARED_LIBS=OFF -DBUILD_TESTS=OFF -DWITH_OPENMP=OFF ..; \ + make; \ + make DESTDIR=$(NDK_DIR) install; \ + make clean; \ + cd ..; \ + rm -rf build;