utils.liq 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. # Turn a source into an infaillible source.
  2. # by adding blank when the source is not available.
  3. # @param s the source to turn infaillible
  4. # @category Source / Track Processing
  5. def mksafe(~id="mksafe",s)
  6. fallback(id=id,track_sensitive=false,[s,blank(id="safe_blank")])
  7. end
  8. # Alias for the <code>l[k]</code> notation.
  9. # @category List
  10. # @param a Key to look for
  11. # @param l List of pairs (key,value)
  12. def list.assoc(a,l)
  13. l[a]
  14. end
  15. # list.mem_assoc(key,l) returns true if l contains a pair
  16. # (key,value)
  17. # @category List
  18. # @param a Key to look for
  19. # @param l List of pairs (key,value)
  20. def list.mem_assoc(a,l)
  21. def f(cur, el) =
  22. if not cur then
  23. fst(el) == a
  24. else
  25. cur
  26. end
  27. end
  28. list.fold(f, false, l)
  29. end
  30. # Remove a pair from an associative list
  31. # @category List
  32. # @param a Key of pair to be removed
  33. # @param l List of pairs (key,value)
  34. def list.remove_assoc(a,l)
  35. list.remove((a,list.assoc(a,l)),l)
  36. end
  37. # Rewrite metadata on the fly using a list of (target,rules).
  38. # @category Source / Track Processing
  39. # @param l \
  40. # List of (target,value) rewriting rules.
  41. # @param ~insert_missing \
  42. # Treat track beginnings without metadata as having empty ones. \
  43. # The operational order is: \
  44. # create empty if needed, map and strip if enabled.
  45. # @param ~update \
  46. # Only update metadata. \
  47. # If false, only returned values will be set as metadata.
  48. # @param ~strip \
  49. # Completly remove empty metadata. \
  50. # Operates on both empty values and empty metadata chunk.
  51. def rewrite_metadata(l,~insert_missing=true,
  52. ~update=true,~strip=false,
  53. s)
  54. # We don't need to return all values, since
  55. # map_metadata only update returned values.
  56. # So, we simply apply all rewrite rules !
  57. def map(m)
  58. def apply(x)
  59. label = fst(x)
  60. value = snd(x)
  61. (label,value % m)
  62. end
  63. list.map(apply,l)
  64. end
  65. map_metadata(map,insert_missing=insert_missing,
  66. update=update,strip=strip,s)
  67. end
  68. # Add a skip function to a source
  69. # when it does not have one
  70. # by default
  71. # @category Interaction
  72. # @param s The source to attach the command to.
  73. def add_skip_command(s) =
  74. # A command to skip
  75. def skip(_) =
  76. source.skip(s)
  77. "Done!"
  78. end
  79. # Register the command:
  80. server.register(namespace="#{source.id(s)}",
  81. usage="skip",
  82. description="Skip the current song.",
  83. "skip",skip)
  84. end
  85. # Removes all metadata coming from a source
  86. # @category Source / Track Processing
  87. def drop_metadata(s)
  88. map_metadata(fun(_)->[],update=false,strip=true,insert_missing=false,s)
  89. end
  90. # Merge all tracks from a source, provided that it does not fail
  91. # @category Source / Track Processing
  92. def merge_tracks(s)
  93. sequence(merge=true,[s])
  94. end
  95. # Default inputs and outpus
  96. #
  97. # They are called "prefered" but it's not a user preference,
  98. # just a view of what's generally preferable among the available
  99. # modules.
  100. # It is important that input and output preferences are in the
  101. # same order: the chosen I/O should work in the same clock, we don't
  102. # want an ALSA input and OSS output. The only exception is AO:
  103. # it is the default output after dummy, so the input will be a dummy
  104. # when AO is used for output.
  105. output.prefered=output.dummy
  106. %ifdef output.ao
  107. output.prefered=output.ao
  108. %endif
  109. %ifdef output.alsa
  110. output.prefered=output.alsa
  111. %endif
  112. %ifdef output.oss
  113. output.prefered=output.oss
  114. %endif
  115. %ifdef output.portaudio
  116. output.prefered = output.portaudio
  117. %endif
  118. %ifdef output.pulseaudio
  119. output.prefered=output.pulseaudio
  120. %endif
  121. # Output to local audio card using the first available driver in
  122. # pulseaudio, portaudio, oss, alsa, ao, dummy.
  123. # @category Source / Output
  124. def output.prefered(~id="",~fallible=false,
  125. ~on_start={()},~on_stop={()},~start=true,s)
  126. output.prefered(id=id,fallible=fallible,
  127. start=start,on_start=on_start,on_stop=on_stop,
  128. s)
  129. end
  130. def in(~id="",~start=true,~on_start={()},~on_stop={()},~fallible=false)
  131. blank(id=id)
  132. end
  133. %ifdef input.alsa
  134. in = input.alsa
  135. %endif
  136. %ifdef input.oss
  137. in = input.oss
  138. %endif
  139. %ifdef input.portaudio
  140. in = input.portaudio
  141. %endif
  142. %ifdef input.pulseaudio
  143. in = input.pulseaudio
  144. %endif
  145. # Create a source from the first available input driver in
  146. # pulseaudio, portaudio, oss, alsa, blank.
  147. # @category Source / Input
  148. def in(~id="",~start=true,~on_start={()},~on_stop={()},~fallible=false)
  149. in(id=id,start=start,on_start=on_start,on_stop=on_stop,fallible=fallible)
  150. end
  151. # Output a stream using the 'output.prefered' operator. The input source does
  152. # not need to be infallible, blank will just be played during failures.
  153. # @param s the source to output
  154. # @category Source / Output
  155. def out(s)
  156. output.prefered(mksafe(s))
  157. end
  158. # Special track insensitive fallback that always skips current song before switching.
  159. # @category Source / Track Processing
  160. # @param ~input The input source
  161. # @param f The fallback source
  162. def fallback.skip(~input,f)
  163. def transition(a,b) =
  164. source.skip(a)
  165. # This eats the last remaining frame from a
  166. sequence([a,b])
  167. end
  168. fallback(track_sensitive=false,transitions=[transition,transition],[input,f])
  169. end
  170. # Compress and normalize, producing a more uniform and "full" sound.
  171. # @category Source / Sound Processing
  172. # @param s The input source.
  173. def nrj(s)
  174. compress(threshold=-15.,ratio=3.,gain=3.,normalize(s))
  175. end
  176. # Multiband-compression.
  177. # @category Source / Sound Processing
  178. # @param s The input source.
  179. def sky(s)
  180. # 3-band crossover
  181. low = filter.iir.eq.low(frequency = 168.)
  182. mh = filter.iir.eq.high(frequency = 100.)
  183. mid = filter.iir.eq.low(frequency = 1800.)
  184. high = filter.iir.eq.high(frequency = 1366.)
  185. # Add back
  186. add(normalize = false,
  187. [ compress(attack = 100., release = 200., threshold = -20.,
  188. ratio = 6., gain = 6.7, knee = 0.3,
  189. low(s)),
  190. compress(attack = 100., release = 200., threshold = -20.,
  191. ratio = 6., gain = 6.7, knee = 0.3,
  192. mid(mh(s))),
  193. compress(attack = 100., release = 200., threshold = -20.,
  194. ratio = 6., gain = 6.7, knee = 0.3,
  195. high(s))
  196. ])
  197. end
  198. # Simple crossfade.
  199. # @category Source / Track Processing
  200. # @param ~start_next Duration in seconds of the crossed end of track.
  201. # @param ~fade_in Duration of the fade in for next track.
  202. # @param ~fade_out Duration of the fade out for previous track.
  203. # @param ~conservative Always prepare for a premature end-of-track.
  204. # @param s The source to use.
  205. def crossfade(~id="",~conservative=true,
  206. ~start_next=5.,~fade_in=3.,~fade_out=3.,
  207. s)
  208. s = fade.in(duration=fade_in,s)
  209. s = fade.out(duration=fade_out,s)
  210. fader = fun (a,b) -> add(normalize=false,[b,a])
  211. cross(id=id,conservative=conservative,duration=start_next,fader,s)
  212. end
  213. # Append speech-synthesized tracks reading the metadata.
  214. # @category Source / Track Processing
  215. # @param ~pattern Pattern to use
  216. # @param s The source to use
  217. def say_metadata
  218. p = 'say:$(if $(artist),"It was $(artist)$(if $(title),\", $(title)\").")'
  219. fun (s,~pattern=p) ->
  220. append(s,fun (m) -> request.queue(queue=[request.create(pattern % m)],
  221. interactive=false))
  222. end
  223. %ifdef soundtouch
  224. # Increases the pitch, making voices sound like on helium.
  225. # @category Source / Sound Processing
  226. # @param s The input source.
  227. def helium(s)
  228. soundtouch(pitch=1.5,s)
  229. end
  230. %endif
  231. # Return true if process exited with 0 code. Command should return quickly.
  232. # @category System
  233. # @param command Command to test
  234. def test_process(command)
  235. lines =
  236. get_process_lines("(" ^ command ^ " >/dev/null 2>&1 && echo 0) || echo 1")
  237. if list.length(lines) == 0 then
  238. false
  239. else
  240. "0" == list.hd(lines)
  241. end
  242. end
  243. # Split an url of the form foo?arg=bar&arg2=bar2
  244. # into ("foo",[("arg","bar"),("arg2","bar2")]).
  245. # @category String
  246. # @param uri Url to split
  247. def url.split(uri) =
  248. ret = string.extract(pattern="([^\?]*)\?(.*)",uri)
  249. args = ret["2"]
  250. if args != "" then
  251. l = string.split(separator="&",args)
  252. def f(x) =
  253. ret = string.split(separator="=",x)
  254. (url.decode(list.nth(ret,0)),
  255. url.decode(list.nth(ret,1)))
  256. end
  257. l = list.map(f,l)
  258. (ret["1"],l)
  259. else
  260. (uri,[])
  261. end
  262. end
  263. # Register a server/telnet command to update a source's metadata. Returns
  264. # a new source, which will receive the updated metadata. The command has
  265. # the following format: insert key1="val1",key2="val2",...
  266. # @category Source / Track Processing
  267. # @param ~id Force the value of the source ID.
  268. def server.insert_metadata(~id="",s) =
  269. x = insert_metadata(id=id,s)
  270. insert = fst(x)
  271. s = snd(x)
  272. def insert(s) =
  273. l = string.split(separator='([^=]+\s*=\s*"(\\"|[^"])*")\s*,\s*',s)
  274. def f(l,x) =
  275. sub = fun (s) -> string.replace(pattern='\\"',fun (_) -> '"',s)
  276. if x != "" then
  277. ret = string.extract(pattern='([^=]+)\s*=\s*"((?:\\"|[^"])*)"',x)
  278. if ret["1"] != "" then
  279. list.append(l,[(ret["1"],
  280. sub(ret["2"]))])
  281. else
  282. l
  283. end
  284. else
  285. l
  286. end
  287. end
  288. meta = list.fold(f,[],l)
  289. if meta != [] then
  290. insert(meta)
  291. "Done"
  292. else
  293. "Syntax error or no metadata given. \
  294. Use key1=\"val1\",key2=\"val2\",.."
  295. end
  296. end
  297. id = source.id(s)
  298. server.register(namespace="#{id}",
  299. description="Insert a metadata chunk.",
  300. usage="insert key1=\"val1\",key2=\"val2\",..",
  301. "insert",insert)
  302. s
  303. end
  304. # Register a command that outputs the RMS of the returned source.
  305. # @category Source / Visualization
  306. # @param ~id Force the value of the source ID.
  307. def server.rms(~id="",s) =
  308. x = rms(id=id,s)
  309. rms = fst(x)
  310. s = snd(x)
  311. id = source.id(s)
  312. def rms(_) =
  313. rms = rms()
  314. "#{rms}"
  315. end
  316. server.register(namespace="#{id}",
  317. description="Return the current RMS of the source.",
  318. usage="rms",
  319. "rms",rms)
  320. s
  321. end
  322. # Read some value from standard input (console).
  323. # @category System
  324. # @param ~hide Hide typed characters (for passwords).
  325. def read(~hide=false)
  326. if hide then
  327. system("stty -echo")
  328. end
  329. s = list.hd(get_process_lines("read BLA && echo $BLA"))
  330. if hide then
  331. system("stty echo")
  332. end
  333. print("")
  334. s
  335. end
  336. # Dummy implementation of file.mime
  337. # @category System
  338. def file.mime_default(_)
  339. ""
  340. end
  341. %ifdef file.mime
  342. # Alias of file.mime (because it is available)
  343. # @category System
  344. def file.mime_default(file)
  345. file.mime(file)
  346. end
  347. %endif
  348. # Generic mime test. First try to use file.mime if it exist.
  349. # Otherwise try to get the value using the file binary.
  350. # Returns "" (empty string) if no value can be find.
  351. # @category System
  352. # @param file The file to test
  353. def get_mime(file) =
  354. def file_method(file) =
  355. if test_process("which file") then
  356. list.hd(get_process_lines("file -b --mime-type \
  357. #{quote(file)}"))
  358. else
  359. ""
  360. end
  361. end
  362. # First try mime method
  363. ret = file.mime_default(file)
  364. if ret != "" then
  365. ret
  366. else
  367. # Now try file method
  368. file_method(file)
  369. end
  370. end
  371. # Remove low frequencies often produced by microphones.
  372. # @category Source / Sound Processing
  373. # @param s The input source.
  374. def mic_filter(s)
  375. filter(freq=200.,q=1.,mode="high",s)
  376. end
  377. # Creates a source that fails to produce anything.
  378. # @category Source / Input
  379. def fail(~id="")
  380. fallback(id=id,[])
  381. end
  382. # Creates a source that plays only one track of the input source.
  383. # @category Source / Track Processing
  384. # @param s The input source.
  385. def once(s)
  386. sequence([s,fail()])
  387. end
  388. # Crossfade between tracks, taking the respective volume levels into account in
  389. # the choice of the transition.
  390. # @category Source / Track Processing
  391. # @param ~start_next Crossing duration, if any.
  392. # @param ~fade_in Fade-in duration, if any.
  393. # @param ~fade_out Fade-out duration, if any.
  394. # @param ~width Width of the volume analysis window.
  395. # @param ~conservative Always prepare for a premature end-of-track.
  396. # @param ~default Transition used when no rule applies \
  397. # (default: sequence).
  398. # @param ~high Value, in dB, for loud sound level.
  399. # @param ~medium Value, in dB, for medium sound level.
  400. # @param ~margin Margin to detect sources that have too different \
  401. # sound level for crossing.
  402. # @param s The input source.
  403. def smart_crossfade (~start_next=5.,~fade_in=3.,~fade_out=3.,
  404. ~default=(fun (a,b) -> sequence([a, b])),
  405. ~high=-15., ~medium=-32., ~margin=4.,
  406. ~width=2.,~conservative=true,s)
  407. fade.out = fade.out(type="sin",duration=fade_out)
  408. fade.in = fade.in(type="sin",duration=fade_in)
  409. add = fun (a,b) -> add(normalize=false,[b, a])
  410. log = log(label="smart_crossfade")
  411. def transition(a,b,ma,mb,sa,sb)
  412. list.iter(fun(x)-> log(level=4,"Before: #{x}"),ma)
  413. list.iter(fun(x)-> log(level=4,"After : #{x}"),mb)
  414. if
  415. # If A and B are not too loud and close, fully cross-fade them.
  416. a <= medium and b <= medium and abs(a - b) <= margin
  417. then
  418. log("Old <= medium, new <= medium and |old-new| <= margin.")
  419. log("Old and new source are not too loud and close.")
  420. log("Transition: crossed, fade-in, fade-out.")
  421. add(fade.out(sa),fade.in(sb))
  422. elsif
  423. # If B is significantly louder than A, only fade-out A.
  424. # We don't want to fade almost silent things, ask for >medium.
  425. b >= a + margin and a >= medium and b <= high
  426. then
  427. log("new >= old + margin, old >= medium and new <= high.")
  428. log("New source is significantly louder than old one.")
  429. log("Transition: crossed, fade-out.")
  430. add(fade.out(sa),sb)
  431. elsif
  432. # Opposite as the previous one.
  433. a >= b + margin and b >= medium and a <= high
  434. then
  435. log("old >= new + margin, new >= medium and old <= high")
  436. log("Old source is significantly louder than new one.")
  437. log("Transition: crossed, fade-in.")
  438. add(sa,fade.in(sb))
  439. elsif
  440. # Do not fade if it's already very low.
  441. b >= a + margin and a <= medium and b <= high
  442. then
  443. log("new >= old + margin, old <= medium and new <= high.")
  444. log("Do not fade if it's already very low.")
  445. log("Transition: crossed, no fade.")
  446. add(sa,sb)
  447. # What to do with a loud end and a quiet beginning ?
  448. # A good idea is to use a jingle to separate the two tracks,
  449. # but that's another story.
  450. else
  451. # Otherwise, A and B are just too loud to overlap nicely,
  452. # or the difference between them is too large and overlapping would
  453. # completely mask one of them.
  454. log("No transition: using default.")
  455. default(sa, sb)
  456. end
  457. end
  458. smart_cross(width=width, duration=start_next, conservative=conservative,
  459. transition,s)
  460. end
  461. # Custom playlist source written using the script language.
  462. # Will read directory or playlist, play all files and stop.
  463. # Returns a pair @(reload,source)@ where @reload@ is a function
  464. # of type @(?uri:string)->unit@ used to reload the source and @source@
  465. # is the actual source. The reload function can optionally be called
  466. # with a new playlist URI. Otherwise, it reloads the previous URI.
  467. # @category Source / Input
  468. # @param ~id Force the value of the source ID.
  469. # @param ~random Randomize playlist content
  470. # @param ~on_done Function to execute when the playlist is finished
  471. # @param uri Playlist URI
  472. def playlist.reloadable(~id="",~random=false,~on_done={()},uri)
  473. # A reference to the playlist
  474. playlist = ref []
  475. # A reference to the uri
  476. playlist_uri = ref uri
  477. # A reference to know if the source
  478. # has been stopped
  479. has_stopped = ref false
  480. # The next function
  481. def next () =
  482. file =
  483. if list.length(!playlist) > 0 then
  484. ret = list.hd(!playlist)
  485. playlist := list.tl(!playlist)
  486. ret
  487. else
  488. # Playlist finished
  489. if not !has_stopped then
  490. on_done ()
  491. end
  492. has_stopped := true
  493. ""
  494. end
  495. request.create(file)
  496. end
  497. # Instanciate the source
  498. source = request.dynamic(id=id,next)
  499. # Get its id.
  500. id = source.id(source)
  501. # The load function
  502. def load_playlist () =
  503. files =
  504. if test_process("test -d #{quote(!playlist_uri)}") then
  505. log(label=id,"playlist is a directory.")
  506. get_process_lines("find #{quote(!playlist_uri)} -type f | sort")
  507. else
  508. playlist = request.create.raw(!playlist_uri)
  509. result =
  510. if request.resolve(playlist) then
  511. playlist = request.filename(playlist)
  512. files = playlist.parse(playlist)
  513. def file_request(el) =
  514. meta = fst(el)
  515. file = snd(el)
  516. s = list.fold(fun (cur, el) ->
  517. "#{cur},#{fst(el)}=#{string.escape(snd(el))}", "", meta)
  518. if s == "" then
  519. file
  520. else
  521. "annotate:#{s}:#{file}"
  522. end
  523. end
  524. list.map(file_request,files)
  525. else
  526. log(label=id,"Couldn't read playlist: request resolution failed.")
  527. []
  528. end
  529. request.destroy(playlist)
  530. result
  531. end
  532. if random then
  533. playlist := list.sort(fun (x,y) -> int_of_float(random.float()), files)
  534. else
  535. playlist := files
  536. end
  537. end
  538. # The reload function
  539. def reload(~uri="") =
  540. if uri != "" then
  541. playlist_uri := uri
  542. end
  543. log(label=id,"Reloading playlist with URI #{!playlist_uri}")
  544. has_stopped := false
  545. load_playlist()
  546. end
  547. # Load the playlist
  548. load_playlist()
  549. # Return
  550. (reload,source)
  551. end
  552. # Custom playlist source written using the script language.
  553. # Will read directory or playlist, play all files and stop
  554. # @category Source / Input
  555. # @param ~id Force the value of the source ID.
  556. # @param ~random Randomize playlist content
  557. # @param ~on_done Function to execute when the playlist is finished
  558. # @param uri Playlist URI
  559. def playlist.once(~id="",~random=false,~on_done={()},uri)
  560. snd(playlist.reloadable(id=id,random=random,on_done=on_done,uri))
  561. end
  562. # Mixes two streams, with faded transitions between the state when only the
  563. # normal stream is available and when the special stream gets added on top of
  564. # it.
  565. # @category Source / Track Processing
  566. # @param ~delay Delay before starting the special source.
  567. # @param ~p Portion of amplitude of the normal source in the mix.
  568. # @param ~normal The normal source, which could be called the carrier too.
  569. # @param ~special The special source.
  570. def smooth_add(~delay=0.5,~p=0.2,~normal,~special)
  571. d = delay
  572. fade.final = fade.final(duration=d*2.)
  573. fade.initial = fade.initial(duration=d*2.)
  574. q = 1. - p
  575. c = amplify
  576. fallback(track_sensitive=false,
  577. [special,normal],
  578. transitions=[
  579. fun(normal,special)->
  580. add(normalize=false,
  581. [c(p,normal),
  582. c(q,fade.final(type="sin",normal)),
  583. sequence([blank(duration=d),c(q,special)])]),
  584. fun(special,normal)->
  585. add(normalize=false,
  586. [c(p,normal),
  587. c(q,fade.initial(type="sin",normal))])
  588. ])
  589. end
  590. # Restrict a source to play only when a predicate is true.
  591. # @category Source / Track Processing
  592. # @param pred The predicate, typically a time interval such as \
  593. # <code>{10h-10h30}</code>.
  594. def at(pred,s)
  595. switch([(pred,s)])
  596. end
  597. # Execute a given action when a predicate is true.
  598. # This will be run in background.
  599. # @category System
  600. # @param ~freq Frequency for checking the predicate, in seconds.
  601. # @param ~pred Predicate indicating when to execute the function, \
  602. # typically a time interval such as <code>{10h-10h30}</code>.
  603. # @param f Function to execute when the predicate is true.
  604. def exec_at(~freq=1.,~pred,f)
  605. def check()
  606. if pred() then
  607. f()
  608. end
  609. freq
  610. end
  611. add_timeout(freq,check)
  612. end
  613. # Register the replaygain protocol.
  614. # @category Liquidsoap
  615. def replaygain_protocol(arg,delay)
  616. # The extraction program
  617. extract_replaygain = "#{configure.libdir}/extract-replaygain"
  618. x = get_process_lines("#{extract_replaygain} #{quote(arg)}")
  619. if list.hd(x) != "" then
  620. ["annotate:replay_gain=\"#{list.hd(x)}\":#{arg}"]
  621. else
  622. [arg]
  623. end
  624. end
  625. add_protocol("replay_gain", replaygain_protocol)
  626. # Enable replay gain metadata resolver. This resolver will
  627. # process any file decoded by liquidsoap and add a replay_gain
  628. # metadata when this value could be computed. For a finer-grained
  629. # replay gain processing, use the replay_gain protocol.
  630. # @category Liquidsoap
  631. # @param ~extract_replaygain The extraction program
  632. def enable_replaygain_metadata(
  633. ~extract_replaygain="#{configure.libdir}/extract-replaygain")
  634. def replaygain_metadata(file)
  635. x = get_process_lines("#{extract_replaygain} \
  636. #{quote(file)}")
  637. if list.hd(x) != "" then
  638. [("replay_gain",list.hd(x))]
  639. else
  640. []
  641. end
  642. end
  643. add_metadata_resolver("replay_gain", replaygain_metadata)
  644. end
  645. # Assign a new clock to the given source (and to other time-dependent
  646. # sources) and return the source. It is a conveniency wrapper around
  647. # clock.assign_new(), allowing more concise scripts in some cases.
  648. # @category Liquidsoap
  649. # @param ~sync Do not synchronize the clock on regular wallclock time, \
  650. # but try to run as fast as possible (CPU burning mode).
  651. def clock(~sync=true,~id="",s)
  652. clock.assign_new(sync=sync,id=id,[s])
  653. s
  654. end
  655. # Create a log of clock times for all the clocks initially present.
  656. # The log is in a simple format which you can directly use with gnuplot.
  657. # @category Liquidsoap
  658. # @param ~interval Polling interval.
  659. # @param ~delay Delay before setting up the clock logger. This should \
  660. # be used to ensure that the logger starts only after \
  661. # the clocks are created.
  662. # @param unlabeled Path of the log file.
  663. def log_clocks(~delay=0.,~interval=1.,logfile)
  664. # Get the current clocks
  665. clocks = list.map(fst,get_clock_status())
  666. # Column headers
  667. system("echo \# #{string.concat(separator=' ',clocks)} > #{(logfile:string)}")
  668. def report()
  669. status = get_clock_status()
  670. status = list.map(fun (x) -> (fst(x),string_of(snd(x))), status)
  671. status = list.map(fun (c) -> status[c], clocks)
  672. system("echo #{string.concat(separator=' ',status)} >> #{logfile}")
  673. interval
  674. end
  675. if delay<=0. then
  676. add_timeout(interval,report)
  677. else
  678. add_timeout(delay,{add_timeout(interval,report) (-1.)})
  679. end
  680. end