@@ -68,16 +68,27 @@ public static function fetch(Beatmapset $beatmapset): ?static
6868 }
6969 }
7070
71- private static function convertAudioForPreview (string $ audioFile , int $ previewTime ): ?string
71+ private static function convertAudioForPreview (string $ audioFile , ? int $ previewTime ): ?string
7272 {
7373 $ srcFile = tmpfile ();
7474 fwrite ($ srcFile , $ audioFile );
7575 $ srcFilename = get_stream_filename ($ srcFile );
76+ $ srcFilenameEscaped = escapeshellarg ($ srcFilename );
7677 $ dstFile = tmpfile ();
7778 $ dstFilename = get_stream_filename ($ dstFile );
7879
7980 $ duration = 10000 ;
80- $ previewTime = max ($ previewTime , 0 );
81+ if ($ previewTime === null || $ previewTime < 0 ) {
82+ $ srcDuration = (float ) exec (implode (' ' , [
83+ 'timeout 10s ' ,
84+ 'ffprobe ' ,
85+ '-loglevel quiet ' ,
86+ "-i {$ srcFilenameEscaped }" ,
87+ '-show_entries format=duration ' ,
88+ '-of csv=p=0 ' ,
89+ ]));
90+ $ previewTime = 0.4 * $ srcDuration * 100 ;
91+ }
8192
8293 $ fadeInExtension = min ($ previewTime , 100 );
8394 $ fadeIn = $ fadeInExtension + 100 ;
@@ -87,9 +98,16 @@ private static function convertAudioForPreview(string $audioFile, int $previewTi
8798 $ fadeOut = $ duration - 1000 ;
8899
89100 $ filter = implode (', ' , [
90- // unify output to 44.1kHz stereo
101+ // unify output to stereo
102+ // resample here for correct loudnorm operation
103+ 'aresample=resampler=soxr:ochl=stereo ' ,
104+ // TODO: two-pass normalisation
105+ // It requires ffmpeg release after 2026-02-20 for saner output parsing.
106+ // Reference: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/21766
107+ 'loudnorm=i=-14 ' ,
108+ // unify output to 44.1kHz (loudnorm above resamples to 192kHz)
91109 // note that vorbis doesn't have bit depth
92- 'aresample=osr=44100:ochl=stereo ' ,
110+ 'aresample=resampler=soxr:osr=44100 ' ,
93111 "afade=t=in:st=0:d= {$ fadeIn }ms:curve=ipar " ,
94112 "afade=t=out:st= {$ fadeOut }ms:d=1000ms:curve=tri " ,
95113 ]);
@@ -101,7 +119,7 @@ private static function convertAudioForPreview(string $audioFile, int $previewTi
101119 '-nostdin ' ,
102120 "-ss {$ previewTime }ms " ,
103121 "-t {$ duration }ms " ,
104- ' -i ' . escapeshellarg ( $ srcFilename ) ,
122+ " -i { $ srcFilenameEscaped }" ,
105123 "-af {$ filter }" ,
106124 '-map 0:a ' , // strip out non-audio streams
107125 '-map_metadata -1 ' , // strip out metadata
@@ -139,7 +157,8 @@ public function osuFileList()
139157 {
140158 return $ this ->osuFileList ??= array_values (array_unique ([
141159 // use db order
142- ...($ this ->beatmapset ?->beatmaps->pluck ('filename ' ) ?? []),
160+ // filename column in beatmaps table is nullable
161+ ...array_reject_null ($ this ->beatmapset ?->beatmaps->pluck ('filename ' ) ?? []),
143162 ...preg_grep ('/\.osu$/i ' , $ this ->fileList ()),
144163 ]));
145164 }
@@ -168,10 +187,10 @@ public function generateAudioPreview(): ?string
168187 $ previewTime = $ parsedFile ->previewTime ;
169188 $ audioFilename = $ parsedFile ->audioFilename ;
170189
171- if (isset ($ audioFilename, $ previewTime )) {
190+ if (isset ($ audioFilename )) {
172191 $ audioFile = $ this ->readFile ($ audioFilename );
173192
174- if ($ audioFile !== null ) {
193+ if ($ audioFile !== false ) {
175194 return static ::convertAudioForPreview ($ audioFile , $ previewTime );
176195 }
177196 }
0 commit comments