#define UNICODE #define _UNICODE #include #include #include #include #include #include #include #define MAX_LINE 2048 #define BUF_SIZE 131072 #define PATH_BUF 32768 typedef struct { double silence_db; int silence_min_ms; double padding_sec; double target_lufs; double true_peak_db; char mp3_bitrate[32]; int skip_existing; int recursive; } Config; static Config g_cfg = {-45.0,300,1.0,-14.0,-1.0,"320k",1,1}; static wchar_t g_ffmpeg[PATH_BUF]; static wchar_t g_ffprobe[PATH_BUF]; static FILE *g_log = NULL; static long g_done = 0, g_total = 0; static void trim(char *s) { char *p=s; while (*p==' '||*p=='\t'||*p=='\r'||*p=='\n') p++; if (p!=s) memmove(s,p,strlen(p)+1); size_t n=strlen(s); while(n && (s[n-1]==' '||s[n-1]=='\t'||s[n-1]=='\r'||s[n-1]=='\n')) s[--n]=0; } static int ieq(const char *a,const char *b){ while(*a&&*b){ char ca=*a,cb=*b; if(ca>='A'&&ca<='Z')ca+=32; if(cb>='A'&&cb<='Z')cb+=32; if(ca!=cb)return 0; a++;b++; } return *a==0&&*b==0; } static void load_config_from(const wchar_t *exe_dir){ wchar_t path[PATH_BUF]; _snwprintf(path,PATH_BUF-1,L"%ls\\config.ini",exe_dir); path[PATH_BUF-1]=0; FILE *f=_wfopen(path,L"rb"); if(!f) return; char line[MAX_LINE]; while(fgets(line,sizeof(line),f)){ trim(line); if(!line[0]||line[0]=='#'||line[0]==';') continue; char *eq=strchr(line,'='); if(!eq) continue; *eq=0; char *k=line,*v=eq+1; trim(k); trim(v); if(ieq(k,"silence_db")) g_cfg.silence_db=strtod(v,NULL); else if(ieq(k,"silence_min_ms")) g_cfg.silence_min_ms=atoi(v); else if(ieq(k,"padding_sec")) g_cfg.padding_sec=strtod(v,NULL); else if(ieq(k,"target_lufs")) g_cfg.target_lufs=strtod(v,NULL); else if(ieq(k,"true_peak_db")) g_cfg.true_peak_db=strtod(v,NULL); else if(ieq(k,"mp3_bitrate")){ strncpy(g_cfg.mp3_bitrate,v,sizeof(g_cfg.mp3_bitrate)-1); g_cfg.mp3_bitrate[sizeof(g_cfg.mp3_bitrate)-1]=0; } else if(ieq(k,"skip_existing")) g_cfg.skip_existing=atoi(v)!=0; else if(ieq(k,"recursive")) g_cfg.recursive=atoi(v)!=0; } fclose(f); } static int has_ext(const wchar_t *name){ const wchar_t *p=wcsrchr(name,L'.'); if(!p) return 0; p++; const wchar_t *ok[]={L"mp3",L"flac",L"wav",L"m4a",L"aac",L"ogg",L"opus",L"wma",NULL}; for(int i=0;ok[i];i++) if(_wcsicmp(p,ok[i])==0) return 1; return 0; } static void path_join(wchar_t *out,size_t cap,const wchar_t *a,const wchar_t *b){ size_t n=wcslen(a); _snwprintf(out,cap-1,L"%ls%ls%ls",a,(n&&a[n-1]==L'\\')?L"":L"\\",b); out[cap-1]=0; } static int file_exists(const wchar_t *p){ DWORD a=GetFileAttributesW(p); return a!=INVALID_FILE_ATTRIBUTES && !(a&FILE_ATTRIBUTE_DIRECTORY); } static int dir_exists(const wchar_t *p){ DWORD a=GetFileAttributesW(p); return a!=INVALID_FILE_ATTRIBUTES && (a&FILE_ATTRIBUTE_DIRECTORY); } /* Create parent directories only. IMPORTANT: never create the output filename as a directory. */ static int ensure_dir(const wchar_t *path){ wchar_t tmp[PATH_BUF]; wcsncpy(tmp,path,PATH_BUF-1); tmp[PATH_BUF-1]=0; size_t n=wcslen(tmp); while(n>0 && (tmp[n-1]==L'\\'||tmp[n-1]==L'/')) tmp[--n]=0; if(!tmp[0]) return 1; size_t start=0; if(n>=3 && tmp[1]==L':' && (tmp[2]==L'\\'||tmp[2]==L'/')) start=3; for(size_t i=start;i=0 && m>=0 && m<60 && sec>=0.0 && sec<60.0) return (double)h*3600.0+(double)m*60.0+sec; } return 0.0; } static double get_duration(const wchar_t *in){ wchar_t cmd[PATH_BUF]; char buf[8192]={0}; /* First choice: ffprobe. Use a format that returns only the number. */ if(file_exists(g_ffprobe)){ _snwprintf(cmd,PATH_BUF-1, L"\"%ls\" -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 \"%ls\" 2>NUL", g_ffprobe,in); cmd[PATH_BUF-1]=0; if(run_capture(cmd,buf,sizeof(buf))>=0){ char *p=buf; while(*p==' '||*p=='\t'||*p=='\r'||*p=='\n') p++; double d=strtod(p,NULL); if(d>0.0) return d; } } /* * Fallback: ffmpeg itself always knows the input duration and prints it * as "Duration: HH:MM:SS.xx". This also makes the program work when a * user has only ffmpeg.exe next to SilenceTrim.exe. */ if(file_exists(g_ffmpeg)){ memset(buf,0,sizeof(buf)); _snwprintf(cmd,PATH_BUF-1, L"\"%ls\" -hide_banner -i \"%ls\" -f null NUL 2>&1", g_ffmpeg,in); cmd[PATH_BUF-1]=0; if(run_capture(cmd,buf,sizeof(buf))>=0){ char *p=strstr(buf,"Duration:"); if(p){ double d=parse_hms_duration(p+9); if(d>0.0) return d; } } } return 0.0; } /* * silencedetect reports every silent section, including silence in the middle. * Only accept a leading section whose start is essentially zero, and a trailing * section whose end is essentially the file duration. This prevents an internal * pause from being mistaken for the beginning/end of a song. */ static void detect_edges(const wchar_t *in,double dur,double *start,double *end){ *start=0; *end=dur; if(dur<=0) return; wchar_t cmd[PATH_BUF]; char buf[BUF_SIZE]={0}; _snwprintf(cmd,PATH_BUF-1, L"\"%ls\" -hide_banner -nostats -i \"%ls\" -af silencedetect=noise=%.2fdB:d=%.3f -f null NUL 2>&1", g_ffmpeg,in,g_cfg.silence_db,g_cfg.silence_min_ms/1000.0); cmd[PATH_BUF-1]=0; if(run_capture(cmd,buf,sizeof(buf))<0) return; double first_start=-1.0, last_end=-1.0; char *p=buf; while((p=strstr(p,"silence_start:"))){ double x=strtod(p+14,NULL); if(first_start<0) first_start=x; p+=14; } p=buf; while((p=strstr(p,"silence_end:"))){ char *q=p+12; double x=strtod(q,NULL); last_end=x; p+=12; } double tol=0.25; if(first_start>=0 && first_start<=tol) *start=first_start; if(last_end>=0 && last_end>=dur-tol) *end=last_end; if(*start<0 || *start>dur) *start=0; if(*end<0 || *end>dur) *end=dur; if(*end<*start){*start=0;*end=dur;} } static const wchar_t *codec_args(const wchar_t *ext,wchar_t *buf,size_t cap){ if(_wcsicmp(ext,L"mp3")==0){ _snwprintf(buf,cap-1,L"-c:a libmp3lame -b:a %hs",g_cfg.mp3_bitrate); } else if(_wcsicmp(ext,L"flac")==0) wcscpy(buf,L"-c:a flac"); else if(_wcsicmp(ext,L"wav")==0) wcscpy(buf,L"-c:a pcm_s16le"); else if(_wcsicmp(ext,L"m4a")==0||_wcsicmp(ext,L"aac")==0) wcscpy(buf,L"-c:a aac -b:a 256k"); else if(_wcsicmp(ext,L"ogg")==0) wcscpy(buf,L"-c:a libvorbis -q:a 6"); else if(_wcsicmp(ext,L"opus")==0) wcscpy(buf,L"-c:a libopus -b:a 160k"); else if(_wcsicmp(ext,L"wma")==0) wcscpy(buf,L"-c:a wmav2 -b:a 192k"); else wcscpy(buf,L"-c:a libmp3lame -b:a 320k"); buf[cap-1]=0; return buf; } static int process_file(const wchar_t *in,const wchar_t *out){ double dur=get_duration(in); if(dur<=0){ logw(L"ERROR|%ls|duration unavailable",in); return -1; } double ss=0,ee=dur; detect_edges(in,dur,&ss,&ee); double kept=ee-ss; if(kept<0.01){ ss=0; ee=dur; } wchar_t ext[16]=L"mp3"; const wchar_t *dot=wcsrchr(in,L'.'); if(dot) { wcsncpy(ext,dot+1,15); ext[15]=0; } if(!ensure_parent_dir(out)){ logw(L"ERROR|%ls|cannot create output directory",in); return -1; } wchar_t tmp[PATH_BUF]; _snwprintf(tmp,PATH_BUF-1,L"%ls.__stmp_%lu_%lu.%ls", out,(unsigned long)GetCurrentProcessId(),(unsigned long)GetTickCount(),ext); tmp[PATH_BUF-1]=0; wchar_t filter[8192]; /* * Add padding after edge trimming. The middle of the song is untouched. * aresample is before loudnorm so all formats have a predictable sample rate. */ _snwprintf(filter,8191, L"[0:a]atrim=start=%.6f:end=%.6f,asetpts=PTS-STARTPTS[a];" L"anullsrc=r=48000:cl=stereo:d=%.6f[pre];" L"anullsrc=r=48000:cl=stereo:d=%.6f[post];" L"[pre][a][post]concat=n=3:v=0:a=1,aresample=48000," L"loudnorm=I=%.2f:TP=%.2f:LRA=11:linear=true", ss,ee,g_cfg.padding_sec,g_cfg.padding_sec,g_cfg.target_lufs,g_cfg.true_peak_db); filter[8191]=0; wchar_t cargs[256]; codec_args(ext,cargs,256); wchar_t cmd[PATH_BUF]; _snwprintf(cmd,PATH_BUF-1, L"\"%ls\" -hide_banner -nostats -loglevel error -y -i \"%ls\" " L"-filter_complex \"%ls\" %ls \"%ls\"", g_ffmpeg,in,filter,cargs,tmp); cmd[PATH_BUF-1]=0; int rc=run_cmd(cmd); if(rc!=0){ DeleteFileW(tmp); logw(L"ERROR|%ls|ffmpeg rc=%d|trim %.3f-%.3f",in,rc,ss,ee); return -1; } if(!MoveFileExW(tmp,out,MOVEFILE_REPLACE_EXISTING)){ DWORD e=GetLastError(); DeleteFileW(tmp); logw(L"ERROR|%ls|move failed Win32=%lu",in,(unsigned long)e); return -1; } logw(L"OK|%ls|trim %.3f-%.3f|pad %.3f|target %.2f|tp %.2f", in,ss,ee,g_cfg.padding_sec,g_cfg.target_lufs,g_cfg.true_peak_db); return 1; } static long count_files(const wchar_t *dir){ WIN32_FIND_DATAW fd; wchar_t pat[PATH_BUF]; path_join(pat,PATH_BUF,dir,L"*"); HANDLE h=FindFirstFileW(pat,&fd); if(h==INVALID_HANDLE_VALUE) return 0; long n=0; do{ if(!wcscmp(fd.cFileName,L".")||!wcscmp(fd.cFileName,L"..")) continue; wchar_t p[PATH_BUF]; path_join(p,PATH_BUF,dir,fd.cFileName); if(fd.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY){ if(g_cfg.recursive) n+=count_files(p); } else if(has_ext(fd.cFileName)) n++; }while(FindNextFileW(h,&fd)); FindClose(h); return n; } static void process_dir(const wchar_t *in,const wchar_t *root_in,const wchar_t *root_out){ WIN32_FIND_DATAW fd; wchar_t pat[PATH_BUF]; path_join(pat,PATH_BUF,in,L"*"); HANDLE h=FindFirstFileW(pat,&fd); if(h==INVALID_HANDLE_VALUE) return; do{ if(!wcscmp(fd.cFileName,L".")||!wcscmp(fd.cFileName,L"..")) continue; wchar_t src[PATH_BUF]; path_join(src,PATH_BUF,in,fd.cFileName); if(fd.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY){ if(g_cfg.recursive) process_dir(src,root_in,root_out); continue; } if(!has_ext(fd.cFileName)) continue; const wchar_t *relptr=src+wcslen(root_in); while(*relptr==L'\\'||*relptr==L'/') relptr++; wchar_t dst[PATH_BUF]; path_join(dst,PATH_BUF,root_out,relptr); if(g_cfg.skip_existing && file_exists(dst)){ g_done++; wprintf(L"[%ld/%ld] SKIP %ls\n",g_done,g_total,src); logw(L"SKIP|%ls|already exists",src); continue; } g_done++; wprintf(L"[%ld/%ld] %ls\n",g_done,g_total,src); int r=process_file(src,dst); if(r<0) wprintf(L" ERROR (see silencetrim.log)\n"); else wprintf(L" OK\n"); }while(FindNextFileW(h,&fd)); FindClose(h); } int wmain(int argc,wchar_t **argv){ SetConsoleOutputCP(CP_UTF8); wchar_t exe_path[PATH_BUF], exe_dir[PATH_BUF]; GetModuleFileNameW(NULL,exe_path,PATH_BUF); exe_path[PATH_BUF-1]=0; wcsncpy(exe_dir,exe_path,PATH_BUF-1); exe_dir[PATH_BUF-1]=0; wchar_t *slash=wcsrchr(exe_dir,L'\\'); if(slash) *slash=0; else wcscpy(exe_dir,L"."); load_config_from(exe_dir); find_tools(exe_dir); if(argc<2){ wprintf(L"SilenceTrim.exe INPUT_FOLDER [OUTPUT_FOLDER]\n"); return 1; } wchar_t in[PATH_BUF],out[PATH_BUF]; if(!GetFullPathNameW(argv[1],PATH_BUF,in,NULL)){ wprintf(L"Input path error.\n"); return 2; } if(argc>=3){ if(!GetFullPathNameW(argv[2],PATH_BUF,out,NULL)){ wprintf(L"Output path error.\n"); return 2; } } else { _snwprintf(out,PATH_BUF-1,L"%ls_Trimmed",in); out[PATH_BUF-1]=0; } if(!dir_exists(in)){ wprintf(L"Input folder not found: %ls\n",in); return 2; } if(!ensure_dir(out)){ wprintf(L"Cannot create output folder: %ls\n",out); return 3; } g_total=count_files(in); g_done=0; wchar_t logp[PATH_BUF]; path_join(logp,PATH_BUF,out,L"silencetrim.log"); g_log=_wfopen(logp,L"a, ccs=UTF-8"); wprintf(L"SilenceTrim (fixed)\n" L"Input : %ls\n" L"Output: %ls\n" L"Files : %ld\n" L"Pad : %.2f sec\n" L"LUFS : %.2f\n" L"TP : %.2f dBTP\n\n", in,out,g_total,g_cfg.padding_sec,g_cfg.target_lufs,g_cfg.true_peak_db); logw(L"--- start --- files=%ld",g_total); if(g_total==0) { wprintf(L"No supported audio files found.\n"); } else { process_dir(in,in,out); } logw(L"--- end --- processed=%ld",g_done); if(g_log) fclose(g_log); wprintf(L"\nFinished. %ld/%ld entries handled.\n",g_done,g_total); return 0; }