diff --git a/README b/README index c50f7cc..56e37fc 100644 --- a/README +++ b/README @@ -97,6 +97,7 @@ Options: (in sequence of usefulness) You can supply this option multiple times: you'll get a drop-down select on the HTTP status page. -p : Port to run the HTTP status server on. + -g : 'gapless': convolve adjacent files (experimental). -D : Moderate volume Folve debug messages to syslog. Can then also be toggled in the UI. -f : Operate in foreground; useful for debugging. diff --git a/file-handler.h b/file-handler.h index a14da6b..9cc1f70 100644 --- a/file-handler.h +++ b/file-handler.h @@ -22,7 +22,8 @@ // Status about some handler, filled in by various subsystem. struct HandlerStats { HandlerStats() - : duration_seconds(-1), progress(-1), status(OPEN), last_access(0) {} + : duration_seconds(-1), progress(-1), status(OPEN), last_access(0), + in_gapless(false), out_gapless(false) {} std::string filename; // filesystem name. std::string format; // File format info if recognized. std::string message; // Per file (error) message if any. @@ -32,8 +33,11 @@ struct HandlerStats { enum Status { OPEN, IDLE, RETIRED }; Status status; // Status of this file handler. double last_access; // Last access in hi-res seconds since epoch. + bool in_gapless; // Were we handed a processor to continue. + bool out_gapless; // Did we pass on our processor. }; +class SoundProcessor; // A handler that deals with operations on files. Since we only provide read // access, this is limited to very few operations. // Closing in particular is not done by this file handler as it might @@ -52,6 +56,7 @@ class FileHandler { // Get handler status. virtual void GetHandlerStatus(struct HandlerStats *s) = 0; + virtual bool AcceptProcessor(SoundProcessor *s) { return false; } private: const int filter_id_; diff --git a/folve-filesystem.cc b/folve-filesystem.cc index c4caf97..09e3ae2 100644 --- a/folve-filesystem.cc +++ b/folve-filesystem.cc @@ -16,6 +16,7 @@ #include "folve-filesystem.h" #include +#include #include #include #include @@ -107,8 +108,9 @@ class SndFileHandler : // convolution filter configuration available. // "partial_file_info" will be set to information known so far, including // error message. - static FileHandler *Create(int filedes, const char *fs_path, - const char *underlying_file, + static FileHandler *Create(FolveFilesystem *fs, + int filedes, const char *fs_path, + const std::string &underlying_file, int filter_id, const std::string &zita_config_dir, HandlerStats *partial_file_info) { @@ -116,7 +118,7 @@ class SndFileHandler : memset(&in_info, 0, sizeof(in_info)); SNDFILE *snd = sf_open_fd(filedes, SFM_READ, &in_info, 0); if (snd == NULL) { - DebugLogf("File %s: %s", underlying_file, sf_strerror(NULL)); + DebugLogf("File %s: %s", underlying_file.c_str(), sf_strerror(NULL)); partial_file_info->message = sf_strerror(NULL); return NULL; } @@ -150,19 +152,19 @@ class SndFileHandler : &config_path); if (found_config) { DebugLogf("File %s, %.1fkHz, %d Bit, %d:%02d: filter config %s", - underlying_file, in_info.samplerate / 1000.0, bits, + underlying_file.c_str(), in_info.samplerate / 1000.0, bits, seconds / 60, seconds % 60, config_path.c_str()); } else { DebugLogf("File %s: couldn't find filter config %s...%s", - underlying_file, + underlying_file.c_str(), path_choices[0].c_str(), path_choices[max_choice].c_str()); partial_file_info->message = "Missing ( " + path_choices[0] + "
... " + path_choices[max_choice] + " )"; sf_close(snd); return NULL; } - return new SndFileHandler(fs_path, filter_id, + return new SndFileHandler(fs, fs_path, filter_id, underlying_file, filedes, snd, in_info, *partial_file_info, config_path); } @@ -238,11 +240,12 @@ class SndFileHandler : private: // TODO(hzeller): trim parameter list. - SndFileHandler(const char *fs_path, int filter_id, - const char *underlying_file, int filedes, SNDFILE *snd_in, + SndFileHandler(FolveFilesystem *fs, const char *fs_path, int filter_id, + const std::string &underlying_file, + int filedes, SNDFILE *snd_in, const SF_INFO &in_info, const HandlerStats &file_info, const std::string &config_path) - : FileHandler(filter_id), + : FileHandler(filter_id), fs_(fs), filedes_(filedes), snd_in_(snd_in), in_info_(in_info), config_path_(config_path), base_stats_(file_info), @@ -333,11 +336,30 @@ class SndFileHandler : out_buffer->HeaderFinished(); } + virtual bool AcceptProcessor(SoundProcessor *processor) { + if (processor_ != NULL || !input_frames_left_) + return false; // We already have one. + processor_ = processor; + if (!processor_->is_input_buffer_complete()) { + input_frames_left_ -= processor_->FillBuffer(snd_in_); + } + base_stats_.in_gapless = true; + return true; + } + + static std::string Dirname(const std::string &filename) { + return filename.substr(0, filename.find_last_of('/') + 1); + } + virtual bool AddMoreSoundData() { + if (processor_ && processor_->pending_writes() > 0) { + processor_->WriteProcessed(snd_out_, processor_->pending_writes()); + return input_frames_left_; + } if (!input_frames_left_) return false; if (!processor_) { - // First time we're called. + // First time we're called and we don't have any processor yet. processor_ = SoundProcessor::Create(config_path_, in_info_.samplerate, in_info_.channels); if (processor_ == NULL) { @@ -360,14 +382,38 @@ class SndFileHandler : Close(); return false; } - processor_->WriteProcessed(snd_out_, r); input_frames_left_ -= r; + if (!input_frames_left_ && !processor_->is_input_buffer_complete() + && fs_->gapless_processing()) { + typedef std::set DirSet; + DirSet dirents; + fs_->ListDirectory(Dirname(base_stats_.filename), &dirents); + DirSet::const_iterator found = dirents.upper_bound(base_stats_.filename); + FileHandler *next_file = NULL; + const bool passed_processor + = (found != dirents.end() + && (next_file = fs_->GetOrCreateHandler(found->c_str())) + && next_file->AcceptProcessor(processor_)); + if (passed_processor) { + DebugLogf("Gapless pass-on from '%s' to '%s'", + base_stats_.filename.c_str(), found->c_str()); + } + processor_->WriteProcessed(snd_out_, r); + if (passed_processor) { + base_stats_.out_gapless = true; + processor_ = NULL; + } + if (next_file) fs_->Close(found->c_str(), next_file); + } else { + processor_->WriteProcessed(snd_out_, r); + } if (input_frames_left_ == 0) { Close(); } return input_frames_left_; } + // TODO add as a utility function to ConversionBuffer ? void CopyBytes(int fd, off_t pos, ConversionBuffer *out, size_t len) { char buf[256]; while (len > 0) { @@ -466,6 +512,7 @@ class SndFileHandler : return memcmp(flac_magic, "fLaC", sizeof(flac_magic)) == 0; } + FolveFilesystem *const fs_; const int filedes_; SNDFILE *const snd_in_; const SF_INFO in_info_; @@ -488,19 +535,20 @@ class SndFileHandler : } // namespace FolveFilesystem::FolveFilesystem() - : current_cfg_index_(0), debug_ui_enabled_(false), + : current_cfg_index_(0), debug_ui_enabled_(false), gapless_processing_(false), open_file_cache_(3), total_file_openings_(0), total_file_reopen_(0) { config_dirs_.push_back(""); // The first config is special: empty. } -FileHandler *FolveFilesystem::CreateFromDescriptor(int filedes, - int cfg_idx, - const char *fs_path, - const char *underlying_file) { +FileHandler *FolveFilesystem::CreateFromDescriptor( + int filedes, + int cfg_idx, + const char *fs_path, + const std::string &underlying_file) { HandlerStats file_info; file_info.filename = fs_path; if (cfg_idx != 0) { - FileHandler *filter = SndFileHandler::Create(filedes, fs_path, + FileHandler *filter = SndFileHandler::Create(this, filedes, fs_path, underlying_file, cfg_idx, config_dirs()[cfg_idx], @@ -520,19 +568,18 @@ std::string FolveFilesystem::CacheKey(int config_idx, const char *fs_path) { return result; } -// Implementation of the C functions in filter-interface.h -FileHandler *FolveFilesystem::CreateHandler(const char *fs_path, - const char *underlying_path) { +FileHandler *FolveFilesystem::GetOrCreateHandler(const char *fs_path) { const int config_idx = current_cfg_index_; const std::string cache_key = CacheKey(config_idx, fs_path); + const std::string underlying_file = underlying_dir() + fs_path; FileHandler *handler = open_file_cache_.FindAndPin(cache_key); if (handler == NULL) { - int filedes = open(underlying_path, O_RDONLY); + int filedes = open(underlying_file.c_str(), O_RDONLY); if (filedes < 0) return NULL; ++total_file_openings_; handler = CreateFromDescriptor(filedes, config_idx, - fs_path, underlying_path); + fs_path, underlying_file); handler = open_file_cache_.InsertPinned(cache_key, handler); } else { ++total_file_reopen_; @@ -555,6 +602,20 @@ void FolveFilesystem::Close(const char *fs_path, const FileHandler *handler) { open_file_cache_.Unpin(cache_key); } +bool FolveFilesystem::ListDirectory(const std::string &fs_dir, + std::set *files) { + const std::string real_dir = underlying_dir() + fs_dir; + DIR *dp = opendir(real_dir.c_str()); + if (dp == NULL) return false; + struct dirent *dent; + while ((dent = readdir(dp)) != NULL) { + std::string x = fs_dir + dent->d_name; + files->insert(fs_dir + dent->d_name); + } + closedir(dp); + return true; +} + void FolveFilesystem::SwitchCurrentConfigIndex(int i) { if (i < 0 || i >= (int) config_dirs_.size()) return; diff --git a/folve-filesystem.h b/folve-filesystem.h index ab2cd79..5e68df8 100644 --- a/folve-filesystem.h +++ b/folve-filesystem.h @@ -21,6 +21,7 @@ #include #include +#include #include "file-handler-cache.h" #include "file-handler.h" @@ -56,16 +57,19 @@ class FolveFilesystem { // Create a new filter given the filesystem path and the underlying // path. // Returns NULL, if it cannot be created. - FileHandler *CreateHandler(const char *fs_path, - const char *underlying_path); - - // Return dynamic size of file. - int StatByFilename(const char *fs_path, struct stat *st); + FileHandler *GetOrCreateHandler(const char *fs_path); // Inform filesystem that this file handler is not needed anymore // (FS still might consider keeping it around for a while). void Close(const char *fs_path, const FileHandler *handler); + // Return dynamic size of file. + int StatByFilename(const char *fs_path, struct stat *st); + + // List files in given filesystem directory. Returns a set of filesystem + // paths of existing files. + bool ListDirectory(const std::string &fs_dir, std::set *files); + FileHandlerCache *handler_cache() { return &open_file_cache_; } // Sets more fine grained debug log messages. @@ -75,6 +79,9 @@ class FolveFilesystem { void set_debug_ui_enabled(bool b) { debug_ui_enabled_ = b; } bool is_debug_ui_enabled() const { return debug_ui_enabled_; } + void set_gapless_processing(bool b) { gapless_processing_ = b; } + bool gapless_processing() const { return gapless_processing_; } + // Some stats. int total_file_openings() { return total_file_openings_; } int total_file_reopen() { return total_file_reopen_; } @@ -85,12 +92,13 @@ class FolveFilesystem { FileHandler *CreateFromDescriptor(int filedes, int cfg_idx, const char *fs_path, - const char *underlying_file); + const std::string &underlying_file); std::string underlying_dir_; std::vector config_dirs_; int current_cfg_index_; bool debug_ui_enabled_; + bool gapless_processing_; FileHandlerCache open_file_cache_; int total_file_openings_; int total_file_reopen_; diff --git a/folve-main.cc b/folve-main.cc index 43ecef2..0f1b1d3 100644 --- a/folve-main.cc +++ b/folve-main.cc @@ -40,16 +40,6 @@ static struct FolveRuntime { int status_port; } folve_rt; -static int has_suffix_string (const char *str, const char *suffix) { - if (!str || !suffix) - return 0; - size_t str_len = strlen(str); - size_t suffix_len = strlen(suffix); - if (suffix_len > str_len) - return 0; - return strncmp(str + str_len - suffix_len, suffix, suffix_len) == 0; -} - static char *concat_path(char *buf, const char *a, const char *b) { strcpy(buf, a); strcat(buf, b); @@ -58,14 +48,8 @@ static char *concat_path(char *buf, const char *a, const char *b) { // Given a relative path from the root of the mounted file-system, get the // original file from the source filesystem. -// TODO(hzeller): move the ogg.fuse.flac logic into convolver-filesystem. static const char *assemble_orig_path(char *buf, const char *path) { - char *result = concat_path(buf, folve_rt.fs->underlying_dir().c_str(), path); - static const char magic_ogg_rewrite[] = ".ogg.fuse.flac"; - if (has_suffix_string(result, magic_ogg_rewrite)) { - *(result + strlen(result) - strlen(".fuse.flac")) = '\0'; - } - return result; + return concat_path(buf, folve_rt.fs->underlying_dir().c_str(), path); } // Essentially lstat(). Just forward to the original filesystem (this @@ -96,19 +80,12 @@ static int folve_readdir(const char *path, void *buf, fuse_fill_dir_t filler, if (dp == NULL) return -errno; - char ogg_rewrite_buffer[PATH_MAX]; - while ((de = readdir(dp)) != NULL) { struct stat st; memset(&st, 0, sizeof(st)); st.st_ino = de->d_ino; st.st_mode = de->d_type << 12; const char *entry_name = de->d_name; - if (has_suffix_string(entry_name, ".ogg")) { - // For ogg files, we pretend they actually end with 'flac', because - // we transparently rewrite them. - entry_name = concat_path(ogg_rewrite_buffer, entry_name, ".fuse.flac"); - } if (filler(buf, entry_name, &st, 0)) break; } @@ -139,9 +116,7 @@ static int folve_open(const char *path, struct fuse_file_info *fi) { // The file-handle has the neat property to be 64 bit - so we can actually // stuff a pointer to our file handler object in there :) // (Yay, someone was thinking while developing that API). - char path_buf[PATH_MAX]; - const char *orig_path = assemble_orig_path(path_buf, path); - FileHandler *handler = folve_rt.fs->CreateHandler(path, orig_path); + FileHandler *handler = folve_rt.fs->GetOrCreateHandler(path); if (handler == NULL) return -errno; fi->fh = (uint64_t) handler; @@ -212,6 +187,7 @@ static int usage(const char *prg) { "\t you'll get a drop-down select on the HTTP " "status page.\n" "\t-p : Port to run the HTTP status server on.\n" + "\t-g : 'gapless': convolve adjacent files (experimental).\n" "\t-D : Moderate volume Folve debug messages to syslog.\n" "\t Can then also be toggled in the UI.\n" "\t-f : Operate in foreground; useful for debugging.\n" @@ -232,6 +208,7 @@ enum { FOLVE_OPT_PORT = 42, FOLVE_OPT_CONFIG, FOLVE_OPT_DEBUG, + FOLVE_OPT_GAPLESS, }; int FolveOptionHandling(void *data, const char *arg, int key, @@ -257,6 +234,9 @@ int FolveOptionHandling(void *data, const char *arg, int key, rt->fs->set_debug_ui_enabled(true); rt->fs->SetDebugMode(true); return 0; + case FOLVE_OPT_GAPLESS: + rt->fs->set_gapless_processing(true); + return 0; } return 1; } @@ -273,6 +253,7 @@ int main(int argc, char *argv[]) { FUSE_OPT_KEY("-p ", FOLVE_OPT_PORT), FUSE_OPT_KEY("-c ", FOLVE_OPT_CONFIG), FUSE_OPT_KEY("-D", FOLVE_OPT_DEBUG), + FUSE_OPT_KEY("-g", FOLVE_OPT_GAPLESS), FUSE_OPT_END }; struct fuse_args args = FUSE_ARGS_INIT(argc, argv); diff --git a/sound-processor.h b/sound-processor.h index 5882f9f..88499c8 100644 --- a/sound-processor.h +++ b/sound-processor.h @@ -39,6 +39,12 @@ class SoundProcessor { return zita_config_.fragm == input_pos_; } + // Number of samples that are pending. Typically once we have this passed + // over to a new file. + int pending_writes() const { + return output_pos_ >= 0 ? zita_config_.fragm - output_pos_ : 0; + } + // Write number of processed samples out to given soundfile. Processes // the data first if necessary. assert(), that there is at least 1 sample // to process. @@ -57,6 +63,8 @@ class SoundProcessor { const ZitaConfig zita_config_; float *const buffer_; const int channels_; + // TODO: instead of two positions, better have one position and two states + // READ, WRITE int input_pos_; int output_pos_; // written position. -1, if not processed yet. float max_out_value_observed_; diff --git a/status-server.cc b/status-server.cc index b11ef26..6f02ebb 100644 --- a/status-server.cc +++ b/status-server.cc @@ -133,13 +133,15 @@ void StatusServer::RetireHandlerEvent(FileHandler *handler) { } static const char sMessageRowHtml[] = - "%s%s" + "%s%s" "-"; static const char sProgressRowHtml[] = - "%s" - "
\n" + "%s" + "%s" // gapless marker + "
\n" "
 
\n
" + "%s" // gapless "%2d:%02d/%2d:%02d"; static void AppendFileInfo(std::string *result, const char *progress_style, @@ -160,7 +162,9 @@ static void AppendFileInfo(std::string *result, const char *progress_style, const int secs = stats.duration_seconds; const int fract_sec = stats.progress * secs; Appendf(result, sProgressRowHtml, status, + stats.in_gapless ? "→" : "", kProgressWidth, (int) (100 * stats.progress), progress_style, + stats.out_gapless ? "→" : "", fract_sec / 60, fract_sec % 60, secs / 60, secs % 60); } Appendf(result, " %s ", @@ -256,8 +260,13 @@ const std::string &StatusServer::CreatePage() { Appendf(&content_, "

Accessed Recently

\n%zd in recency cache\n", stat_list.size()); + if (filesystem_->gapless_processing()) { + content_.append("
'→' denote gapless transfers\n"); + } content_.append("\n"); - Appendf(&content_, "" + Appendf(&content_, "" + "" // progress bar. + "" "" "\n", kProgressWidth); CompareStats comparator;
StatProgress
StatProgressPosLenFormatFile