From d1a48e4d0636450578d3b5215b17987e405d1be7 Mon Sep 17 00:00:00 2001 From: Romain Beauxis Date: Fri, 14 Apr 2023 17:44:21 -0500 Subject: [PATCH] Add pcm s16 format --- CHANGES.md | 2 + src/core/decoder/external_decoder.ml | 8 +- src/core/decoder/ffmpeg_decoder.ml | 37 ++-- src/core/decoder/ffmpeg_internal_decoder.ml | 56 +++++- src/core/decoder/ffmpeg_internal_decoder.mli | 1 + src/core/decoder/gstreamer_decoder.ml | 9 +- src/core/decoder/liq_ogg_decoder.ml | 8 +- src/core/decoder/wav_aiff_decoder.ml | 10 +- src/core/dune | 4 + src/core/encoder.ml | 165 +++++++++--------- src/core/encoder.mli | 6 +- src/core/encoder/ffmpeg_internal_encoder.ml | 63 ++++++- src/core/encoder_formats/ffmpeg_format.ml | 1 + src/core/lang.ml | 1 + src/core/lang.mli | 4 + src/core/lang_encoder.ml | 26 ++- src/core/lang_encoders/lang_avi.ml | 3 +- .../lang_encoders/lang_external_encoder.ml | 4 +- src/core/lang_encoders/lang_fdkaac.ml | 4 +- src/core/lang_encoders/lang_ffmpeg.ml | 70 ++++++-- src/core/lang_encoders/lang_flac.ml | 5 +- src/core/lang_encoders/lang_gstreamer.ml | 5 +- src/core/lang_encoders/lang_mp3.ml | 4 +- src/core/lang_encoders/lang_ogg.ml | 4 +- src/core/lang_encoders/lang_opus.ml | 4 +- src/core/lang_encoders/lang_shine.ml | 4 +- src/core/lang_encoders/lang_speex.ml | 4 +- src/core/lang_encoders/lang_theora.ml | 3 +- src/core/lang_encoders/lang_vorbis.ml | 4 +- src/core/lang_encoders/lang_wav.ml | 4 +- src/core/modules.ml | 4 + src/core/operators/time_warp.ml | 33 ++-- src/core/operators/track_map.ml | 154 ++++++++++++++++ src/core/source.ml | 11 +- src/core/sources/audio_gen.ml | 25 ++- src/core/sources/blank.ml | 8 + src/core/sources/noise.ml | 15 ++ src/core/stream/content_pcm_base.ml | 102 +++++++++++ src/core/stream/content_pcm_f32.ml | 50 ++++++ src/core/stream/content_pcm_f32.mli | 35 ++++ src/core/stream/content_pcm_s16.ml | 54 ++++++ src/core/stream/content_pcm_s16.mli | 36 ++++ src/core/stream/frame_base.ml | 17 ++ src/core/stream/mFrame.ml | 7 +- src/core/stream/mFrame.mli | 4 +- src/core/stream/vFrame.ml | 6 +- src/core/stream/vFrame.mli | 4 +- src/core/types/format_type.ml | 112 +++++++----- src/core/types/format_type.mli | 9 +- src/core/types/frame_type.ml | 9 +- src/core/types/frame_type.mli | 3 + src/libs/audio.liq | 32 ++++ tests/language/encoders.liq | 41 +++++ tests/media/dune.inc | 139 +++++++++++++++ tests/media/gen_dune.ml | 3 + tests/media/test_pcm_f32_decode.liq | 7 + tests/media/test_pcm_s16_decode.liq | 7 + tests/regression/dune.inc | 13 ++ tests/regression/unified-pcm-types.liq | 19 ++ tests/streams/dune.inc | 69 ++++++++ tests/streams/sine.detect.full_conv.liq | 16 ++ tests/streams/sine.detect.pcm_f32.liq | 13 ++ tests/streams/sine.detect.pcm_s16.liq | 13 ++ 63 files changed, 1348 insertions(+), 245 deletions(-) create mode 100644 src/core/operators/track_map.ml create mode 100644 src/core/stream/content_pcm_base.ml create mode 100644 src/core/stream/content_pcm_f32.ml create mode 100644 src/core/stream/content_pcm_f32.mli create mode 100644 src/core/stream/content_pcm_s16.ml create mode 100644 src/core/stream/content_pcm_s16.mli create mode 100755 tests/media/test_pcm_f32_decode.liq create mode 100755 tests/media/test_pcm_s16_decode.liq create mode 100644 tests/regression/unified-pcm-types.liq create mode 100755 tests/streams/sine.detect.full_conv.liq create mode 100755 tests/streams/sine.detect.pcm_f32.liq create mode 100755 tests/streams/sine.detect.pcm_s16.liq diff --git a/CHANGES.md b/CHANGES.md index a8e8b1c937..0eb263e1c8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,8 @@ New: +- Added support for less memory hungry audio formats, namely + `pcm_s16` and `pcm_f32` (#3008) - Added support for native osc library (#2426, #2480). - SRT: added support for passphrase, pbkeylen, streamid, added native type for srt sockets with methods, moved stats diff --git a/src/core/decoder/external_decoder.ml b/src/core/decoder/external_decoder.ml index 398d03a19e..793f1ed58d 100644 --- a/src/core/decoder/external_decoder.ml +++ b/src/core/decoder/external_decoder.ml @@ -91,13 +91,7 @@ let create_stream process input = Gc.finalise close ret; ret -let audio_n n = - Content.( - Audio.lift_params - { - Content.channel_layout = - lazy (Audio_converter.Channel_layout.layout_of_channels n); - }) +let audio_n = Frame_base.format_of_channels ~pcm_kind:Content.Audio.kind let test_ctype f filename = (* 0 = file rejected, diff --git a/src/core/decoder/ffmpeg_decoder.ml b/src/core/decoder/ffmpeg_decoder.ml index a2d38e31fa..ef8c070255 100644 --- a/src/core/decoder/ffmpeg_decoder.ml +++ b/src/core/decoder/ffmpeg_decoder.ml @@ -598,16 +598,10 @@ let get_type ~ctype ~url container = Ffmpeg_raw_content.( Audio.lift_params (AudioSpecs.mk_params p))); Frame.Fields.add field format content_type - | p, Some _ -> + | p, Some format -> Frame.Fields.add field - Content.( - Audio.lift_params - { - Content.channel_layout = - lazy - (Audio_converter.Channel_layout.layout_of_channels - (Avcodec.Audio.get_nb_channels p)); - }) + (Frame_base.format_of_channels ~pcm_kind:(Content.kind format) + (Avcodec.Audio.get_nb_channels p)) content_type | _ -> content_type) Frame.Fields.empty audio_streams @@ -790,7 +784,30 @@ let mk_streams ~ctype ~decode_first_metadata container = ( stream, check_metadata stream (Ffmpeg_internal_decoder.mk_audio_decoder ~channels - ~stream ~field params) )) + ~stream ~field ~pcm_kind:Content.Audio.kind params) + )) + streams, + pos + 1 ) + | Some format when Content_pcm_s16.is_format format -> + let channels = Content_pcm_s16.channels_of_format format in + ( Streams.add idx + (`Audio_frame + ( stream, + check_metadata stream + (Ffmpeg_internal_decoder.mk_audio_decoder ~channels + ~stream ~field ~pcm_kind:Content_pcm_s16.kind params) + )) + streams, + pos + 1 ) + | Some format when Content_pcm_f32.is_format format -> + let channels = Content_pcm_f32.channels_of_format format in + ( Streams.add idx + (`Audio_frame + ( stream, + check_metadata stream + (Ffmpeg_internal_decoder.mk_audio_decoder ~channels + ~stream ~field ~pcm_kind:Content_pcm_f32.kind params) + )) streams, pos + 1 ) | _ -> (streams, pos + 1)) diff --git a/src/core/decoder/ffmpeg_internal_decoder.ml b/src/core/decoder/ffmpeg_internal_decoder.ml index c7cb0e2f56..0ecb000593 100644 --- a/src/core/decoder/ffmpeg_internal_decoder.ml +++ b/src/core/decoder/ffmpeg_internal_decoder.ml @@ -26,11 +26,60 @@ open Mm let log = Log.make ["decoder"; "ffmpeg"; "internal"] +module type Converter_type = sig + type t + + module Content : sig + type data + + val lift_data : ?offset:int -> ?length:int -> data -> Content_base.data + end + + val create : + ?options:Swresample.options list -> + Avutil.Channel_layout.t -> + ?in_sample_format:Avutil.Sample_format.t -> + int -> + Avutil.Channel_layout.t -> + ?out_sample_format:Avutil.Sample_format.t -> + int -> + t + + val convert : + ?offset:int -> ?length:int -> t -> Swresample.Frame.t -> Content.data +end + module ConverterInput = Swresample.Make (Swresample.Frame) -module Converter = ConverterInput (Swresample.PlanarFloatArray) + +module Converter = struct + module Content = Content_audio + include ConverterInput (Swresample.PlanarFloatArray) +end + +module Converter_pcm_s16 = struct + module Content = Content_pcm_s16 + include ConverterInput (Swresample.S16PlanarBigArray) +end + +module Converter_pcm_f32 = struct + module Content = Content_pcm_f32 + include ConverterInput (Swresample.FltPlanarBigArray) +end + module Scaler = Swscale.Make (Swscale.Frame) (Swscale.BigArray) -let mk_audio_decoder ~channels ~stream ~field codec = +let mk_audio_decoder ~channels ~stream ~field ~pcm_kind codec = + let converter = + match pcm_kind with + | _ when Content_audio.is_kind pcm_kind -> + (module Converter : Converter_type) + | _ when Content_pcm_s16.is_kind pcm_kind -> + (module Converter_pcm_s16 : Converter_type) + | _ when Content_pcm_f32.is_kind pcm_kind -> + (module Converter_pcm_f32 : Converter_type) + | _ -> raise Content_base.Invalid + in + let module Converter = (val converter : Converter_type) in Ffmpeg_decoder_common.set_audio_stream_decoder stream; let in_sample_rate = ref (Avcodec.Audio.get_sample_rate codec) in let in_channel_layout = ref (Avcodec.Audio.get_channel_layout codec) in @@ -59,7 +108,8 @@ let mk_audio_decoder ~channels ~stream ~field codec = in_sample_format := frame_in_sample_format; converter := mk_converter ()); let content = Converter.convert !converter frame in - buffer.Decoder.put_pcm ~field ~samplerate:target_sample_rate content; + Generator.put buffer.Decoder.generator field + (Converter.Content.lift_data content); let metadata = Avutil.Frame.metadata frame in if metadata <> [] then ( let m = Hashtbl.create (List.length metadata) in diff --git a/src/core/decoder/ffmpeg_internal_decoder.mli b/src/core/decoder/ffmpeg_internal_decoder.mli index 8940269588..b34bc85d15 100644 --- a/src/core/decoder/ffmpeg_internal_decoder.mli +++ b/src/core/decoder/ffmpeg_internal_decoder.mli @@ -24,6 +24,7 @@ val mk_audio_decoder : channels:int -> stream:(Avutil.input, Avutil.audio, [ `Frame ]) Av.stream -> field:Frame.field -> + pcm_kind:Content.kind -> Avutil.audio Avcodec.params -> buffer:Decoder.buffer -> Avutil.audio Avutil.Frame.t -> diff --git a/src/core/decoder/gstreamer_decoder.ml b/src/core/decoder/gstreamer_decoder.ml index a5c1f40087..2bc66247fd 100644 --- a/src/core/decoder/gstreamer_decoder.ml +++ b/src/core/decoder/gstreamer_decoder.ml @@ -277,14 +277,7 @@ let get_type ~channels filename = in let audio = if audio = 0 then None - else - Some - Content.( - Audio.lift_params - { - Content.channel_layout = - lazy (Audio_converter.Channel_layout.layout_of_channels audio); - }) + else Some (Frame_base.format_of_channels ~pcm_kind:Content.Audio.kind audio) in let video = if video = 0 then None else Some Content.(default_format Video.kind) diff --git a/src/core/decoder/liq_ogg_decoder.ml b/src/core/decoder/liq_ogg_decoder.ml index 0ffcd4c88f..b109460a8d 100644 --- a/src/core/decoder/liq_ogg_decoder.ml +++ b/src/core/decoder/liq_ogg_decoder.ml @@ -250,13 +250,7 @@ let file_type ~ctype:_ filename = if audio = 0 then None else Some - Content.( - Audio.lift_params - { - Content.channel_layout = - lazy - (Audio_converter.Channel_layout.layout_of_channels audio); - }) + (Frame_base.format_of_channels ~pcm_kind:Content.Audio.kind audio) in let video = if video = 0 then None else Some Content.(default_format Video.kind) diff --git a/src/core/decoder/wav_aiff_decoder.ml b/src/core/decoder/wav_aiff_decoder.ml index a107b738fb..a6dca8879e 100644 --- a/src/core/decoder/wav_aiff_decoder.ml +++ b/src/core/decoder/wav_aiff_decoder.ml @@ -151,14 +151,8 @@ let file_type ~ctype:_ filename = Some (Frame.Fields.make ~audio: - Content.( - Audio.lift_params - { - Content.channel_layout = - lazy - (Audio_converter.Channel_layout.layout_of_channels - channels); - }) + (Frame_base.format_of_channels ~pcm_kind:Content.Audio.kind + channels) ())) let wav_mime_types = diff --git a/src/core/dune b/src/core/dune index 147fb437ac..dd79eec40c 100644 --- a/src/core/dune +++ b/src/core/dune @@ -68,6 +68,9 @@ compress_exp content content_audio + content_pcm_base + content_pcm_f32 + content_pcm_s16 content_base content_midi content_timed @@ -231,6 +234,7 @@ theora_format time_warp track + track_map tutils type typing diff --git a/src/core/encoder.ml b/src/core/encoder.ml index 2367bb199b..3d65bf3aa5 100644 --- a/src/core/encoder.ml +++ b/src/core/encoder.ml @@ -34,92 +34,99 @@ type format = | External of External_encoder_format.t | GStreamer of Gstreamer_format.t -let audio_type n = +let audio_type ~pcm_kind n = Frame.Fields.make ~audio: (Type.make (Format_type.descr - (`Format - Content.( - Audio.lift_params - { - Content.channel_layout = - lazy (Audio_converter.Channel_layout.layout_of_channels n); - })))) + (`Format (Frame_base.format_of_channels ~pcm_kind n)))) () -let audio_video_type n = +let video_format () = Content.(default_format Video.kind) + +let audio_video_type ~pcm_kind n = Frame.Fields.add Frame.Fields.video - (Type.make - (Format_type.descr (`Format Content.(default_format Video.kind)))) - (audio_type n) - -let type_of_format = function - | WAV w -> audio_type w.Wav_format.channels - | AVI a -> audio_video_type a.Avi_format.channels - | MP3 m -> audio_type (if m.Mp3_format.stereo then 2 else 1) - | Shine m -> audio_type m.Shine_format.channels - | Flac m -> audio_type m.Flac_format.channels - | Ffmpeg m -> - List.fold_left - (fun ctype (field, c) -> - Frame.Fields.add field - (Type.make - (Format_type.descr - (match c with - | `Copy _ -> - `Format - Content.( - default_format (kind_of_string "ffmpeg.copy")) - | `Encode { Ffmpeg_format.mode = `Raw; options = `Audio _ } - -> - `Format - Content.( - default_format (kind_of_string "ffmpeg.audio.raw")) - | `Encode { Ffmpeg_format.mode = `Raw; options = `Video _ } - -> - `Format - Content.( - default_format (kind_of_string "ffmpeg.video.raw")) - | `Encode - Ffmpeg_format. - { mode = `Internal; options = `Audio { channels } } -> - assert (channels > 0); - `Format - Content.( - Audio.lift_params - { - Content.channel_layout = - lazy - (Audio_converter.Channel_layout - .layout_of_channels channels); - }) - | `Encode - { Ffmpeg_format.mode = `Internal; options = `Video _ } - -> - `Format Content.(default_format Video.kind)))) - ctype) - Frame.Fields.empty m.streams - | FdkAacEnc m -> audio_type m.Fdkaac_format.channels - | Ogg { Ogg_format.audio; video } -> - let channels = - match audio with - | Some (Ogg_format.Vorbis { Vorbis_format.channels = n; _ }) - | Some (Ogg_format.Opus { Opus_format.channels = n; _ }) - | Some (Ogg_format.Flac { Flac_format.channels = n; _ }) -> - n - | Some (Ogg_format.Speex { Speex_format.stereo; _ }) -> - if stereo then 2 else 1 - | None -> 0 - in - if video = None then audio_type channels else audio_video_type channels - | External e -> - let channels = e.External_encoder_format.channels in - if e.External_encoder_format.video <> None then audio_video_type channels - else audio_type channels - | GStreamer ({ Gstreamer_format.has_video } as gst) -> - let channels = Gstreamer_format.audio_channels gst in - if has_video then audio_video_type channels else audio_type channels + (Type.make (Format_type.descr (`Format (video_format ())))) + (audio_type ~pcm_kind n) + +let video_type () = + Frame.Fields.make + ~video:(Type.make (Format_type.descr (`Format (video_format ())))) + () + +let type_of_format f = + let audio_type = audio_type ~pcm_kind:Content_audio.kind in + let audio_video_type = audio_video_type ~pcm_kind:Content_audio.kind in + match f with + | WAV w -> audio_type w.Wav_format.channels + | AVI a -> audio_video_type a.Avi_format.channels + | MP3 m -> audio_type (if m.Mp3_format.stereo then 2 else 1) + | Shine m -> audio_type m.Shine_format.channels + | Flac m -> audio_type m.Flac_format.channels + | Ffmpeg m -> + List.fold_left + (fun ctype (field, c) -> + Frame.Fields.add field + (Type.make + (Format_type.descr + (match c with + | `Copy _ -> + `Format + Content.( + default_format (kind_of_string "ffmpeg.copy")) + | `Encode + { Ffmpeg_format.mode = `Raw; options = `Audio _ } -> + `Format + Content.( + default_format (kind_of_string "ffmpeg.audio.raw")) + | `Encode + { Ffmpeg_format.mode = `Raw; options = `Video _ } -> + `Format + Content.( + default_format (kind_of_string "ffmpeg.video.raw")) + | `Encode + Ffmpeg_format. + { + mode = `Internal; + options = `Audio { pcm_kind; channels }; + } -> + assert (channels > 0); + let params = + { + Content.channel_layout = + lazy + (Audio_converter.Channel_layout + .layout_of_channels channels); + } + in + `Format (Frame_base.audio_format ~pcm_kind params) + | `Encode + { Ffmpeg_format.mode = `Internal; options = `Video _ } + -> + `Format Content.(default_format Video.kind)))) + ctype) + Frame.Fields.empty m.streams + | FdkAacEnc m -> audio_type m.Fdkaac_format.channels + | Ogg { Ogg_format.audio; video } -> + let channels = + match audio with + | Some (Ogg_format.Vorbis { Vorbis_format.channels = n; _ }) + | Some (Ogg_format.Opus { Opus_format.channels = n; _ }) + | Some (Ogg_format.Flac { Flac_format.channels = n; _ }) -> + n + | Some (Ogg_format.Speex { Speex_format.stereo; _ }) -> + if stereo then 2 else 1 + | None -> 0 + in + if video = None then audio_type channels else audio_video_type channels + | External e -> + let channels = e.External_encoder_format.channels in + if e.External_encoder_format.video <> None then + audio_video_type channels + else audio_type channels + | GStreamer ({ Gstreamer_format.has_video } as gst) -> + let channels = Gstreamer_format.audio_channels gst in + if has_video then audio_video_type channels else audio_type channels let string_of_format = function | WAV w -> Wav_format.to_string w diff --git a/src/core/encoder.mli b/src/core/encoder.mli index 59df61d56a..8220a31023 100644 --- a/src/core/encoder.mli +++ b/src/core/encoder.mli @@ -34,8 +34,10 @@ type format = | External of External_encoder_format.t | GStreamer of Gstreamer_format.t -val audio_type : int -> Type.t Frame.Fields.t -val audio_video_type : int -> Type.t Frame.Fields.t +val audio_type : pcm_kind:Content.kind -> int -> Type.t Frame.Fields.t +val video_format : unit -> Content.format +val video_type : unit -> Type.t Frame.Fields.t +val audio_video_type : pcm_kind:Content.kind -> int -> Type.t Frame.Fields.t val type_of_format : format -> Type.t Frame.Fields.t val string_of_format : format -> string diff --git a/src/core/encoder/ffmpeg_internal_encoder.ml b/src/core/encoder/ffmpeg_internal_encoder.ml index 841be29058..d1704f79ed 100644 --- a/src/core/encoder/ffmpeg_internal_encoder.ml +++ b/src/core/encoder/ffmpeg_internal_encoder.ml @@ -24,8 +24,43 @@ open Mm (** FFMPEG internal encoder *) -module InternalResampler = - Swresample.Make (Swresample.PlanarFloatArray) (Swresample.Frame) +module type InternalResampler_type = sig + type t + + module Content : sig + type data + + val get_data : Content.data -> data + end + + val create : + ?options:Swresample.options list -> + Avutil__Channel_layout.t -> + ?in_sample_format:Avutil__Sample_format.t -> + int -> + Avutil__Channel_layout.t -> + ?out_sample_format:Avutil__Sample_format.t -> + int -> + t + + val convert : + ?offset:int -> ?length:int -> t -> Content.data -> Swresample.Frame.t +end + +module InternalResampler = struct + module Content = Content_audio + include Swresample.Make (Swresample.PlanarFloatArray) (Swresample.Frame) +end + +module InternalResampler_pcm_s16 = struct + module Content = Content_pcm_s16 + include Swresample.Make (Swresample.S16PlanarBigArray) (Swresample.Frame) +end + +module InternalResampler_pcm_f32 = struct + module Content = Content_pcm_f32 + include Swresample.Make (Swresample.FltPlanarBigArray) (Swresample.Frame) +end module RawResampler = Swresample.Make (Swresample.Frame) (Swresample.Frame) module InternalScaler = Swscale.Make (Swscale.BigArray) (Swscale.Frame) @@ -96,6 +131,19 @@ let write_audio_frame ~time_base ~sample_rate ~channel_layout ~sample_format Avfilter.Utils.convert_audio converter write_frame frame let mk_audio ~mode ~codec ~params ~options ~field output = + let internal_resampler = + match params.Ffmpeg_format.pcm_kind with + | pcm_kind when Content_audio.is_kind pcm_kind -> + (module InternalResampler : InternalResampler_type) + | pcm_kind when Content_pcm_s16.is_kind pcm_kind -> + (module InternalResampler_pcm_s16 : InternalResampler_type) + | pcm_kind when Content_pcm_f32.is_kind pcm_kind -> + (module InternalResampler_pcm_f32 : InternalResampler_type) + | _ -> raise Content_base.Invalid + in + let module InternalResampler = + (val internal_resampler : InternalResampler_type) + in let codec = try Avcodec.Audio.find_encoder_by_name codec with e -> @@ -132,11 +180,9 @@ let mk_audio ~mode ~codec ~params ~options ~field output = fun frame start len -> let astart = Frame.audio_of_main start in let alen = Frame.audio_of_main len in - let content = Generator.get_field frame field in - [ - InternalResampler.convert ~length:alen ~offset:astart resampler - (Content.Audio.get_data content); - ] + let content = Content.sub (Frame.get frame field) astart alen in + let pcm = InternalResampler.Content.get_data content in + [InternalResampler.convert ~length:alen ~offset:0 resampler pcm] in let raw_converter = @@ -377,8 +423,7 @@ let mk_video ~mode ~codec ~params ~options ~field output = fun frame start len -> let vstart = Frame.video_of_main start in let vstop = Frame.video_of_main (start + len) in - let content = Generator.get_field frame field in - let vbuf = Content.Video.get_data content in + let vbuf = VFrame.data ~field frame in for i = vstart to vstop - 1 do let f = Video.Canvas.get vbuf i diff --git a/src/core/encoder_formats/ffmpeg_format.ml b/src/core/encoder_formats/ffmpeg_format.ml index 8588e0e177..fe7ae50004 100644 --- a/src/core/encoder_formats/ffmpeg_format.ml +++ b/src/core/encoder_formats/ffmpeg_format.ml @@ -29,6 +29,7 @@ type opts = (string, opt_val) Hashtbl.t type hwaccel = [ `None | `Auto ] type audio_options = { + pcm_kind : Content.kind; channels : int; samplerate : int Lazy.t; sample_format : string option; diff --git a/src/core/lang.ml b/src/core/lang.ml index b326dafc69..feee9ee8c4 100644 --- a/src/core/lang.ml +++ b/src/core/lang.ml @@ -14,6 +14,7 @@ let add_protocol ~syntax ~doc ~static name resolver = let frame_t base_type fields = Frame_type.make base_type fields let internal_tracks_t () = Frame_type.internal_tracks () +let pcm_audio_t () = Frame_type.pcm_audio () let format_t t = Type.make diff --git a/src/core/lang.mli b/src/core/lang.mli index b89b6a4951..95f97fb5a6 100644 --- a/src/core/lang.mli +++ b/src/core/lang.mli @@ -245,6 +245,10 @@ val frame_t : t -> t Frame.Fields.t -> t applied. Equivalent to: ['a where 'a is an internal media type] *) val internal_tracks_t : unit -> t +(* Return a generic frame type with the pcm audio constraint + applied. *) +val pcm_audio_t : unit -> t + (** [fun_t args r] is the type of a function taking [args] as parameters * and returning values of type [r]. * The elements of [r] are of the form [(b,l,t)] where [b] indicates if diff --git a/src/core/lang_encoder.ml b/src/core/lang_encoder.ml index ef275a5c6d..998b40ae3a 100644 --- a/src/core/lang_encoder.ml +++ b/src/core/lang_encoder.ml @@ -68,12 +68,12 @@ let raise_generic_error (l, t) = (Value.to_string v)) | `Encoder _ -> raise_error ~pos:None "unexpected subencoder" -(** An encoder. *) +(* An encoder. *) type encoder = { + (* Compute the kind of the encoder. *) type_of_encoder : Term.encoder_params -> Type.t Frame.Fields.t; - (** Compute the kind of the encoder. *) + (* Actually create the encoder. *) make : Hooks.encoder_params -> Encoder.format; - (** Actually create the encoder. *) } let encoders = ref [] @@ -93,9 +93,27 @@ let channels_of_params ?(default = 2) p = | "", `Term { term = Ground (String "mono") } -> Some 1 | "stereo", `Term { term = Ground (Bool b); _ } -> Some (if b then 2 else 1) + | "stereo", `Term ({ t = { Type.pos } } as tm) -> + raise_error ~pos + (Printf.sprintf + "Invalid value %s for stereo mode. Only static `true` or \ + `false` are allowed." + (Term.to_string tm)) | "mono", `Term { term = Ground (Bool b); _ } -> Some (if b then 1 else 2) + | "mono", `Term ({ t = { Type.pos } } as tm) -> + raise_error ~pos + (Printf.sprintf + "Invalid value %s for mono mode. Only static `true` or \ + `false` are allowed." + (Term.to_string tm)) | "channels", `Term { term = Ground (Int n) } -> Some n + | "channels", `Term ({ t = { Type.pos } } as tm) -> + raise_error ~pos + (Printf.sprintf + "Invalid value %s for channels mode. Only static numbers are \ + allowed." + (Term.to_string tm)) | _ -> None) p with @@ -103,7 +121,7 @@ let channels_of_params ?(default = 2) p = | None -> default (** Compute a kind from a non-fully evaluated format. This should give the same - result than [Encoder.kind_of_format] once evaluated... *) + result than [Encoder.type_of_format] once evaluated... *) let type_of_encoder ((e, p) : Term.encoder) = (find_encoder e).type_of_encoder p let type_of_encoder ~pos e = diff --git a/src/core/lang_encoders/lang_avi.ml b/src/core/lang_encoders/lang_avi.ml index 4b346637b3..b51685b2f3 100644 --- a/src/core/lang_encoders/lang_avi.ml +++ b/src/core/lang_encoders/lang_avi.ml @@ -51,6 +51,7 @@ let make params = Encoder.AVI avi let type_of_encoder p = - Encoder.audio_video_type (Lang_encoder.channels_of_params p) + Encoder.audio_video_type ~pcm_kind:Content.Audio.kind + (Lang_encoder.channels_of_params p) let () = Lang_encoder.register "avi" type_of_encoder make diff --git a/src/core/lang_encoders/lang_external_encoder.ml b/src/core/lang_encoders/lang_external_encoder.ml index fd3ab47038..aa6defeda5 100644 --- a/src/core/lang_encoders/lang_external_encoder.ml +++ b/src/core/lang_encoders/lang_external_encoder.ml @@ -23,7 +23,9 @@ open Value open Ground -let type_of_encoder p = Encoder.audio_type (Lang_encoder.channels_of_params p) +let type_of_encoder p = + Encoder.audio_type ~pcm_kind:Content.Audio.kind + (Lang_encoder.channels_of_params p) let make params = let defaults = diff --git a/src/core/lang_encoders/lang_fdkaac.ml b/src/core/lang_encoders/lang_fdkaac.ml index 0a6e9a4de5..52a595d441 100644 --- a/src/core/lang_encoders/lang_fdkaac.ml +++ b/src/core/lang_encoders/lang_fdkaac.ml @@ -23,7 +23,9 @@ open Value open Ground -let type_of_encoder p = Encoder.audio_type (Lang_encoder.channels_of_params p) +let type_of_encoder p = + Encoder.audio_type ~pcm_kind:Content.Audio.kind + (Lang_encoder.channels_of_params p) let make params = let valid_samplerates = diff --git a/src/core/lang_encoders/lang_ffmpeg.ml b/src/core/lang_encoders/lang_ffmpeg.ml index 6cc4498c36..d4d36b0556 100644 --- a/src/core/lang_encoders/lang_ffmpeg.ml +++ b/src/core/lang_encoders/lang_ffmpeg.ml @@ -184,25 +184,50 @@ let type_of_encoder = | `Audio -> let channels = try - let channels = - try List.assoc "channels" args - with Not_found -> List.assoc "ac" args + let name, channels = + try ("channels", List.assoc "channels" args) + with Not_found -> ("ac", List.assoc "ac" args) in match channels with | `Term { Term.term = Term.Ground (Int n) } -> n - | _ -> raise Exit - with - | Not_found -> 2 - | Exit -> raise Not_found + | `Term ({ t = { Type.pos } } as tm) -> + Lang_encoder.raise_error ~pos + (Printf.sprintf + "Invalid value %s for %s parameter. \ + Only static numbers are allowed." + name (Term.to_string tm)) + | _ -> + Lang_encoder.raise_error ~pos:None + (Printf.sprintf + "Invalid value for %s parameter." name) + with Not_found -> 2 in - Content.( - Audio.lift_params - { - Content.channel_layout = - lazy - (Audio_converter.Channel_layout - .layout_of_channels channels); - }) + let pcm_kind = + List.fold_left + (fun pcm_kind -> function + | ( "", + `Term + { Term.term = Term.Ground (String "pcm") } + ) -> + Content.Audio.kind + | ( "", + `Term + { + Term.term = + Term.Ground (String "pcm_s16"); + } ) -> + Content_pcm_s16.kind + | ( "", + `Term + { + Term.term = + Term.Ground (String "pcm_f32"); + } ) -> + Content_pcm_f32.kind + | _ -> pcm_kind) + Content.Audio.kind args + in + Frame_base.format_of_channels ~pcm_kind channels | `Video -> Content.(default_format Video.kind)) in let field = Frame.Fields.register field in @@ -240,7 +265,8 @@ let ffmpeg_gen params = let default_audio = { - Ffmpeg_format.channels = 2; + Ffmpeg_format.pcm_kind = Content_audio.kind; + channels = 2; samplerate = Frame.audio_rate; sample_format = None; } @@ -272,6 +298,18 @@ let ffmpeg_gen params = let rec parse_audio_args ~opts options = function | [] -> options (* Audio options *) + | ("", t) :: args when to_string t = "pcm" -> + parse_audio_args ~opts + { options with Ffmpeg_format.pcm_kind = Content_audio.kind } + args + | ("", t) :: args when to_string t = "pcm_s16" -> + parse_audio_args ~opts + { options with Ffmpeg_format.pcm_kind = Content_pcm_s16.kind } + args + | ("", t) :: args when to_string t = "pcm_f32" -> + parse_audio_args ~opts + { options with Ffmpeg_format.pcm_kind = Content_pcm_f32.kind } + args | ("channels", t) :: args -> parse_audio_args ~opts { options with Ffmpeg_format.channels = to_int t } diff --git a/src/core/lang_encoders/lang_flac.ml b/src/core/lang_encoders/lang_flac.ml index 4a4d7543d8..e1d752d972 100644 --- a/src/core/lang_encoders/lang_flac.ml +++ b/src/core/lang_encoders/lang_flac.ml @@ -23,7 +23,10 @@ open Value open Ground -let type_of_encoder p = Encoder.audio_type (Lang_encoder.channels_of_params p) +let type_of_encoder p = + Encoder.audio_type ~pcm_kind:Content.Audio.kind + (Lang_encoder.channels_of_params p) + let accepted_bits_per_sample = [8; 16; 24; 32] let flac_gen params = diff --git a/src/core/lang_encoders/lang_gstreamer.ml b/src/core/lang_encoders/lang_gstreamer.ml index 4e09a52981..fc3d0658bf 100644 --- a/src/core/lang_encoders/lang_gstreamer.ml +++ b/src/core/lang_encoders/lang_gstreamer.ml @@ -33,8 +33,9 @@ let type_of_encoder p = p in let channels = Lang_encoder.channels_of_params p in - if has_video then Encoder.audio_video_type channels - else Encoder.audio_type channels + let pcm_kind = Content.Audio.kind in + if has_video then Encoder.audio_video_type ~pcm_kind channels + else Encoder.audio_type ~pcm_kind channels let make ?pos params = let () = diff --git a/src/core/lang_encoders/lang_mp3.ml b/src/core/lang_encoders/lang_mp3.ml index 2c6eb9472b..3396edb59b 100644 --- a/src/core/lang_encoders/lang_mp3.ml +++ b/src/core/lang_encoders/lang_mp3.ml @@ -23,7 +23,9 @@ open Value open Ground -let type_of_encoder p = Encoder.audio_type (Lang_encoder.channels_of_params p) +let type_of_encoder p = + Encoder.audio_type ~pcm_kind:Content.Audio.kind + (Lang_encoder.channels_of_params p) let allowed_bitrates = [ diff --git a/src/core/lang_encoders/lang_ogg.ml b/src/core/lang_encoders/lang_ogg.ml index cf2495865c..4b298a6e63 100644 --- a/src/core/lang_encoders/lang_ogg.ml +++ b/src/core/lang_encoders/lang_ogg.ml @@ -35,8 +35,8 @@ let type_of_encoder p = let video = List.exists (function "", `Encoder ("theora", _) -> true | _ -> false) p in - if not video then Encoder.audio_type channels - else Encoder.audio_video_type channels + if not video then Encoder.audio_type ~pcm_kind:Content.Audio.kind channels + else Encoder.audio_video_type ~pcm_kind:Content.Audio.kind channels let make p = let ogg_audio e p = diff --git a/src/core/lang_encoders/lang_opus.ml b/src/core/lang_encoders/lang_opus.ml index 9a067d4251..560a550368 100644 --- a/src/core/lang_encoders/lang_opus.ml +++ b/src/core/lang_encoders/lang_opus.ml @@ -23,7 +23,9 @@ open Value open Ground -let type_of_encoder p = Encoder.audio_type (Lang_encoder.channels_of_params p) +let type_of_encoder p = + Encoder.audio_type ~pcm_kind:Content.Audio.kind + (Lang_encoder.channels_of_params p) let make params = let defaults = diff --git a/src/core/lang_encoders/lang_shine.ml b/src/core/lang_encoders/lang_shine.ml index e4298662cb..27deb77a8d 100644 --- a/src/core/lang_encoders/lang_shine.ml +++ b/src/core/lang_encoders/lang_shine.ml @@ -23,7 +23,9 @@ open Value open Ground -let type_of_encoder p = Encoder.audio_type (Lang_encoder.channels_of_params p) +let type_of_encoder p = + Encoder.audio_type ~pcm_kind:Content.Audio.kind + (Lang_encoder.channels_of_params p) let make params = let defaults = diff --git a/src/core/lang_encoders/lang_speex.ml b/src/core/lang_encoders/lang_speex.ml index 17146abe19..08b522b0c2 100644 --- a/src/core/lang_encoders/lang_speex.ml +++ b/src/core/lang_encoders/lang_speex.ml @@ -23,7 +23,9 @@ open Value open Ground -let type_of_encoder p = Encoder.audio_type (Lang_encoder.channels_of_params p) +let type_of_encoder p = + Encoder.audio_type ~pcm_kind:Content.Audio.kind + (Lang_encoder.channels_of_params p) let make params = let defaults = diff --git a/src/core/lang_encoders/lang_theora.ml b/src/core/lang_encoders/lang_theora.ml index 2535167c4a..fd138b77de 100644 --- a/src/core/lang_encoders/lang_theora.ml +++ b/src/core/lang_encoders/lang_theora.ml @@ -23,8 +23,7 @@ open Value open Ground -let type_of_encoder p = - Encoder.audio_video_type (Lang_encoder.channels_of_params p) +let type_of_encoder _ = Encoder.video_type () let make params = let defaults = diff --git a/src/core/lang_encoders/lang_vorbis.ml b/src/core/lang_encoders/lang_vorbis.ml index 4fec54be8e..c39aaea5bd 100644 --- a/src/core/lang_encoders/lang_vorbis.ml +++ b/src/core/lang_encoders/lang_vorbis.ml @@ -23,7 +23,9 @@ open Value open Ground -let type_of_encoder p = Encoder.audio_type (Lang_encoder.channels_of_params p) +let type_of_encoder p = + Encoder.audio_type ~pcm_kind:Content.Audio.kind + (Lang_encoder.channels_of_params p) let make_cbr params = let defaults = diff --git a/src/core/lang_encoders/lang_wav.ml b/src/core/lang_encoders/lang_wav.ml index d1473556cd..62c8b5be57 100644 --- a/src/core/lang_encoders/lang_wav.ml +++ b/src/core/lang_encoders/lang_wav.ml @@ -23,7 +23,9 @@ open Value open Ground -let type_of_encoder p = Encoder.audio_type (Lang_encoder.channels_of_params p) +let type_of_encoder p = + Encoder.audio_type ~pcm_kind:Content.Audio.kind + (Lang_encoder.channels_of_params p) let make params = let defaults = diff --git a/src/core/modules.ml b/src/core/modules.ml index f5b0066bd1..5e195f52e4 100644 --- a/src/core/modules.ml +++ b/src/core/modules.ml @@ -25,7 +25,11 @@ let synth = Lang.add_module "synth" let synth_all = Lang.add_module ~base:synth "all" let thread = Lang.add_module "thread" let track = Lang.add_module "track" +let track_encode = Lang.add_module ~base:track "encode" +let track_decode = Lang.add_module ~base:track "decode" let track_audio = Lang.add_module ~base:track "audio" +let track_encode_audio = Lang.add_module ~base:track_encode "audio" +let track_decode_audio = Lang.add_module ~base:track_decode "audio" let track_video = Lang.add_module ~base:track "video" let track_metadata = Lang.add_module ~base:track "metadata" let video = Lang.add_module "video" diff --git a/src/core/operators/time_warp.ml b/src/core/operators/time_warp.ml index 48c9ef8a2f..010c6a24d5 100644 --- a/src/core/operators/time_warp.ml +++ b/src/core/operators/time_warp.ml @@ -110,7 +110,7 @@ module Buffer = struct (* The kind of value shared by a producer and a consumer. *) type control = { lock : Mutex.t; - generator : Generator.t; + generator : Generator.t Lazy.t; mutable buffering : bool; mutable abort : bool; } @@ -123,21 +123,27 @@ module Buffer = struct inherit Source.source ~name:id () method self_sync = (`Static, false) method stype = `Fallible - method remaining = proceed c (fun () -> Generator.remaining c.generator) + + method remaining = + proceed c (fun () -> Generator.remaining (Lazy.force c.generator)) + method is_ready = proceed c (fun () -> not c.buffering) method seek len = - let len = min (Generator.length c.generator) len in - Generator.truncate c.generator len; + let len = min (Generator.length (Lazy.force c.generator)) len in + Generator.truncate (Lazy.force c.generator) len; len - method buffer_length = Generator.length c.generator + method buffer_length = Generator.length (Lazy.force c.generator) method private get_frame frame = proceed c (fun () -> assert (not c.buffering); - Generator.fill c.generator frame; - if Frame.is_partial frame && Generator.length c.generator = 0 then ( + Generator.fill (Lazy.force c.generator) frame; + if + Frame.is_partial frame + && Generator.length (Lazy.force c.generator) = 0 + then ( self#log#important "Buffer emptied, start buffering..."; c.buffering <- true)) @@ -163,19 +169,20 @@ module Buffer = struct if c.abort then ( c.abort <- false; source#abort_track); - Generator.feed c.generator frame; - if Generator.length c.generator > prebuf then ( + Generator.feed (Lazy.force c.generator) frame; + if Generator.length (Lazy.force c.generator) > prebuf then ( c.buffering <- false; - if Generator.length c.generator > maxbuf then - Generator.truncate c.generator - (Generator.length c.generator - maxbuf))) + if Generator.length (Lazy.force c.generator) > maxbuf then + Generator.truncate (Lazy.force c.generator) + (Generator.length (Lazy.force c.generator) - maxbuf))) end let create ~id ~autostart ~infallible ~on_start ~on_stop ~pre_buffer ~max_buffer source_val = let control = { - generator = Generator.create (Lang.to_source source_val)#content_type; + generator = + lazy (Generator.create (Lang.to_source source_val)#content_type); lock = Mutex.create (); buffering = true; abort = false; diff --git a/src/core/operators/track_map.ml b/src/core/operators/track_map.ml new file mode 100644 index 0000000000..cea8979822 --- /dev/null +++ b/src/core/operators/track_map.ml @@ -0,0 +1,154 @@ +(***************************************************************************** + + Liquidsoap, a programmable audio stream generator. + Copyright 2003-2023 Savonet team + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details, fully stated in the COPYING + file at the root of the liquidsoap distribution. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + *****************************************************************************) + +open Source + +class track_map ~name ~field ~fn s = + object (self) + inherit operator ~name [s] as super + method stype = s#stype + method remaining = s#remaining + method abort_track = s#abort_track + method seek = s#seek + method self_sync = s#self_sync + method is_ready = s#is_ready + val mutable tmp_frame = None + + method tmp_frame = + match tmp_frame with + | Some b -> b + | None -> + let format = Frame.Fields.find field s#content_type in + let f = + Frame.create (Frame.Fields.add field format Frame.Fields.empty) + in + tmp_frame <- Some f; + f + + method private get_frame buf = + let tmp_frame = self#tmp_frame in + let start_pos = Frame.position buf in + assert (start_pos = Frame.position tmp_frame); + s#get tmp_frame; + let end_pos = Frame.position tmp_frame in + let len = end_pos - start_pos in + let src = fn (Content.sub (Frame.get tmp_frame field) start_pos len) in + Content.blit src 0 (Frame.get buf field) start_pos len; + List.iter + (fun (pos, m) -> + if start_pos <= pos && pos < end_pos then + Generator.add_metadata ~pos buf m) + (Frame.get_all_metadata tmp_frame); + Frame.add_break buf end_pos + + method! advance = + super#advance; + Frame.clear self#tmp_frame + end + +let _ = + let content_t = Lang.univ_t () in + let input_t = + Type.( + make (Custom (Format_type.kind_handler (Content_audio.kind, content_t)))) + in + let output_t = + Type.( + make (Custom (Format_type.kind_handler (Content_pcm_s16.kind, content_t)))) + in + Lang.add_track_operator ~base:Modules.track_encode_audio "pcm_s16" + [("", input_t, None, None)] + ~category:`Audio + ~descr:"Encode an audio track using PCM signed 16 bit integers." + ~return_t:output_t + (fun p -> + let field, s = Lang.to_track (Lang.assoc "" 1 p) in + let fn c = + Content_pcm_s16.(lift_data (from_audio (Content.Audio.get_data c))) + in + (field, new track_map ~name:"track.encode.audio.pcm_s16" ~field ~fn s)) + +let _ = + let content_t = Lang.univ_t () in + let input_t = + Type.( + make (Custom (Format_type.kind_handler (Content_pcm_s16.kind, content_t)))) + in + let output_t = + Type.( + make (Custom (Format_type.kind_handler (Content_audio.kind, content_t)))) + in + Lang.add_track_operator ~base:Modules.track_decode_audio "pcm_s16" + [("", input_t, None, None)] + ~category:`Audio + ~descr:"Decode an audio track using PCM signed 16 bit integers." + ~return_t:output_t + (fun p -> + let field, s = Lang.to_track (Lang.assoc "" 1 p) in + let fn c = + Content.Audio.lift_data Content_pcm_s16.(to_audio (get_data c)) + in + (field, new track_map ~name:"track.decode.audio.pcm_s16" ~field ~fn s)) + +let _ = + let content_t = Lang.univ_t () in + let input_t = + Type.( + make (Custom (Format_type.kind_handler (Content_audio.kind, content_t)))) + in + let output_t = + Type.( + make (Custom (Format_type.kind_handler (Content_pcm_f32.kind, content_t)))) + in + Lang.add_track_operator ~base:Modules.track_encode_audio "pcm_f32" + [("", input_t, None, None)] + ~category:`Audio + ~descr:"Encode an audio track using PCM signed 16 bit integers." + ~return_t:output_t + (fun p -> + let field, s = Lang.to_track (Lang.assoc "" 1 p) in + let fn c = + Content_pcm_f32.(lift_data (from_audio (Content.Audio.get_data c))) + in + (field, new track_map ~name:"track.encode.audio.pcm_f32" ~field ~fn s)) + +let _ = + let content_t = Lang.univ_t () in + let input_t = + Type.( + make (Custom (Format_type.kind_handler (Content_pcm_f32.kind, content_t)))) + in + let output_t = + Type.( + make (Custom (Format_type.kind_handler (Content_audio.kind, content_t)))) + in + Lang.add_track_operator ~base:Modules.track_decode_audio "pcm_f32" + [("", input_t, None, None)] + ~category:`Audio + ~descr:"Decode an audio track using PCM signed 16 bit integers." + ~return_t:output_t + (fun p -> + let field, s = Lang.to_track (Lang.assoc "" 1 p) in + let fn c = + Content.Audio.lift_data Content_pcm_f32.(to_audio (get_data c)) + in + (field, new track_map ~name:"track.decode.audio.pcm_f32" ~field ~fn s)) diff --git a/src/core/source.ml b/src/core/source.ml index e7161c6287..f9db8ef063 100644 --- a/src/core/source.ml +++ b/src/core/source.ml @@ -407,9 +407,14 @@ class virtual operator ?(name = "src") sources = ct method private audio_channels = - Content.Audio.channels_of_format - (Option.get - (Frame.Fields.find_opt Frame.Fields.audio self#content_type)) + match Frame.Fields.find_opt Frame.Fields.audio self#content_type with + | Some c when Content.Audio.is_format c -> + Content.Audio.channels_of_format c + | Some c when Content_pcm_s16.is_format c -> + Content_pcm_s16.channels_of_format c + | Some c when Content_pcm_f32.is_format c -> + Content_pcm_f32.channels_of_format c + | _ -> raise Content.Invalid method private video_dimensions = Content.Video.dimensions_of_format diff --git a/src/core/sources/audio_gen.ml b/src/core/sources/audio_gen.ml index 8fd11175dc..a5a3a47988 100644 --- a/src/core/sources/audio_gen.ml +++ b/src/core/sources/audio_gen.ml @@ -26,6 +26,12 @@ open Mm open Source +let to_audio = function + | c when Content.Audio.is_data c -> Content.Audio.get_data c + | c when Content_pcm_s16.is_data c -> Content_pcm_s16.(to_audio (get_data c)) + | c when Content_pcm_f32.is_data c -> Content_pcm_f32.(to_audio (get_data c)) + | _ -> raise Content.Invalid + class gen ~seek name g freq duration ampl = let g = g (freq ()) in object @@ -34,16 +40,27 @@ class gen ~seek name g freq duration ampl = method private synthesize frame off len = let off = Frame.audio_of_main off in let len = Frame.audio_of_main len in - let buf = AFrame.pcm frame in + let content = AFrame.content frame in + let buf = to_audio content in g#set_frequency (freq ()); g#set_volume (ampl ()); - g#fill buf off len + g#fill buf off len; + match content with + | _ when Content.Audio.is_data content -> () + | _ when Content_pcm_s16.is_data content -> + let pcm = Content_pcm_s16.get_data content in + Content_pcm_s16.blit_audio buf off pcm off len; + Frame.set_audio frame (Content_pcm_s16.lift_data pcm) + | _ when Content_pcm_f32.is_data content -> + let pcm = Content_pcm_f32.get_data content in + Content_pcm_f32.blit_audio buf off pcm off len; + Frame.set_audio frame (Content_pcm_f32.lift_data pcm) + | _ -> raise Content.Invalid end let add name g = let return_t = - Lang.frame_t Lang.unit_t - (Frame.Fields.make ~audio:(Format_type.audio ()) ()) + Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Lang.pcm_audio_t ()) ()) in Lang.add_operator name ~category:`Input ~descr:("Generate a " ^ name ^ " wave.") diff --git a/src/core/sources/blank.ml b/src/core/sources/blank.ml index 66f872f162..4aa4a0dfe7 100644 --- a/src/core/sources/blank.ml +++ b/src/core/sources/blank.ml @@ -56,6 +56,14 @@ class blank duration = Audio.clear (Content.Audio.get_data (Frame.get ab field)) audio_pos audio_len + | _ when Content_pcm_s16.is_format typ -> + Content_pcm_s16.clear + (Content_pcm_s16.get_data (Frame.get ab field)) + audio_pos audio_len + | _ when Content_pcm_f32.is_format typ -> + Content_pcm_f32.clear + (Content_pcm_f32.get_data (Frame.get ab field)) + audio_pos audio_len | _ when Content.Video.is_format typ -> Video.Canvas.blank (Content.Video.get_data (Frame.get ab field)) diff --git a/src/core/sources/noise.ml b/src/core/sources/noise.ml index 09db31c8dc..66e53f8923 100644 --- a/src/core/sources/noise.ml +++ b/src/core/sources/noise.ml @@ -41,6 +41,21 @@ class noise duration = Audio.Generator.white_noise (Content.Audio.get_data (Frame.get frame field)) audio_pos audio_len + (* This is not optimal. *) + | _ when Content_pcm_s16.is_format typ -> + let pcm = Content_pcm_s16.get_data (Frame.get frame field) in + let audio = Content_pcm_s16.to_audio pcm in + Audio.Generator.white_noise audio audio_pos audio_len; + Content_pcm_s16.blit_audio audio audio_pos pcm audio_pos + audio_len; + Frame.set frame field (Content_pcm_s16.lift_data pcm) + | _ when Content_pcm_f32.is_format typ -> + let pcm = Content_pcm_f32.get_data (Frame.get frame field) in + let audio = Content_pcm_f32.to_audio pcm in + Audio.Generator.white_noise audio audio_pos audio_len; + Content_pcm_f32.blit_audio audio audio_pos pcm audio_pos + audio_len; + Frame.set frame field (Content_pcm_f32.lift_data pcm) | _ when Content.Video.is_format typ -> Video.Canvas.iter Image.YUV420.randomize (Content.Video.get_data (Frame.get frame field)) diff --git a/src/core/stream/content_pcm_base.ml b/src/core/stream/content_pcm_base.ml new file mode 100644 index 0000000000..83c5411916 --- /dev/null +++ b/src/core/stream/content_pcm_base.ml @@ -0,0 +1,102 @@ +(***************************************************************************** + + Liquidsoap, a programmable audio stream generator. + Copyright 2003-2023 Savonet team + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details, fully stated in the COPYING + file at the root of the liquidsoap distribution. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + *****************************************************************************) + +open Frame_settings + +type params = Content_audio.Specs.params + +let string_of_params = Content_audio.Specs.string_of_params +let merge = Content_audio.Specs.merge +let compatible = Content_audio.Specs.compatible +let clear _ = () + +let blit src src_pos dst dst_pos len = + let ( ! ) = audio_of_main in + let src_pos = !src_pos in + let dst_pos = !dst_pos in + let len = !len in + Array.iter2 + (fun src dst -> + Bigarray.Array1.blit + (Bigarray.Array1.sub src src_pos len) + (Bigarray.Array1.sub dst dst_pos len)) + src dst + +let copy ~fmt = + Array.map (fun c -> + let c' = + Bigarray.Array1.create fmt Bigarray.c_layout (Bigarray.Array1.dim c) + in + Bigarray.Array1.blit c c'; + c) + +let param_of_channels = Content_audio.Specs.param_of_channels +let parse_param = Content_audio.Specs.parse_param +let params d = param_of_channels (Array.length d) + +let default_params _ = + param_of_channels (Lazy.force Frame_settings.audio_channels) + +let make ~fmt ?(length = 0) { Content_audio.Specs.channel_layout } = + let channels = + match !!channel_layout with + | `Mono -> 1 + | `Stereo -> 2 + | `Five_point_one -> 6 + in + Array.init channels (fun _ -> + Bigarray.Array1.create fmt Bigarray.c_layout (audio_of_main length)) + +let length = function + | [||] -> 0 + | c -> main_of_audio (Bigarray.Array1.dim c.(0)) + +let clear_content ~v b ofs len = + Array.iter (fun c -> Bigarray.Array1.fill (Bigarray.Array1.sub c ofs len) v) b + +let from_audio ~to_value ~fmt c = + Array.map + (fun c -> + Bigarray.Array1.init fmt Bigarray.c_layout (Array.length c) (fun pos -> + to_value c.(pos))) + c + +let to_audio ~of_value c = + Array.map + (fun c -> + Array.init (Bigarray.Array1.dim c) (fun pos -> + of_value (Bigarray.Array1.unsafe_get c pos))) + c + +let blit_audio ~to_value src src_ofs dst dst_ofs len = + Array.iter2 + (fun src dst -> + Array.iteri + (fun pos v -> + if src_ofs <= pos && pos < len then + Bigarray.Array1.set dst (dst_ofs + (pos - src_ofs)) (to_value v)) + src) + src dst + +let channels_of_format ~get_params p = + Content_audio.Specs.( + channels_of_param (Lazy.force (get_params p).channel_layout)) diff --git a/src/core/stream/content_pcm_f32.ml b/src/core/stream/content_pcm_f32.ml new file mode 100644 index 0000000000..9abcfd8223 --- /dev/null +++ b/src/core/stream/content_pcm_f32.ml @@ -0,0 +1,50 @@ +(***************************************************************************** + + Liquidsoap, a programmable audio stream generator. + Copyright 2003-2023 Savonet team + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details, fully stated in the COPYING + file at the root of the liquidsoap distribution. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + *****************************************************************************) + +open Content_base + +module Specs = struct + include Content_pcm_base + + type kind = [ `Pcm_f32 ] + + let kind = `Pcm_f32 + let kind_of_string = function "pcm_f32" -> Some `Pcm_f32 | _ -> None + + type data = + (float, Bigarray.float32_elt, Bigarray.c_layout) Bigarray.Array1.t array + + let string_of_kind = function `Pcm_f32 -> "pcm_f32" + let copy = copy ~fmt:Bigarray.float32 + let make = make ~fmt:Bigarray.float32 +end + +include MkContentBase (Specs) + +let kind = lift_kind `Pcm_f32 +let clear = Content_pcm_base.clear_content ~v:0. +let to_value (v : float) = v [@@inline always] +let of_value (v : float) = v [@@inline always] +let from_audio = Content_pcm_base.from_audio ~to_value ~fmt:Bigarray.float32 +let to_audio = Content_pcm_base.to_audio ~of_value +let blit_audio = Content_pcm_base.blit_audio ~to_value +let channels_of_format = Content_pcm_base.channels_of_format ~get_params diff --git a/src/core/stream/content_pcm_f32.mli b/src/core/stream/content_pcm_f32.mli new file mode 100644 index 0000000000..e8978cbfa7 --- /dev/null +++ b/src/core/stream/content_pcm_f32.mli @@ -0,0 +1,35 @@ +(***************************************************************************** + + Liquidsoap, a programmable audio stream generator. + Copyright 2003-2023 Savonet team + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details, fully stated in the COPYING + file at the root of the liquidsoap distribution. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + *****************************************************************************) + +include + Content_base.Content + with type kind = [ `Pcm_f32 ] + and type params = Content_audio.params + and type data = + (float, Bigarray.float32_elt, Bigarray.c_layout) Bigarray.Array1.t array + +val kind : Content_base.kind +val clear : data -> int -> int -> unit +val from_audio : Content_audio.data -> data +val to_audio : data -> Content_audio.data +val blit_audio : Content_audio.data -> int -> data -> int -> int -> unit +val channels_of_format : Content_base.format -> int diff --git a/src/core/stream/content_pcm_s16.ml b/src/core/stream/content_pcm_s16.ml new file mode 100644 index 0000000000..4c7e038f66 --- /dev/null +++ b/src/core/stream/content_pcm_s16.ml @@ -0,0 +1,54 @@ +(***************************************************************************** + + Liquidsoap, a programmable audio stream generator. + Copyright 2003-2023 Savonet team + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details, fully stated in the COPYING + file at the root of the liquidsoap distribution. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + *****************************************************************************) + +open Content_base + +module Specs = struct + include Content_pcm_base + + type kind = [ `Pcm_s16 ] + + let kind = `Pcm_s16 + let kind_of_string = function "pcm_s16" -> Some `Pcm_s16 | _ -> None + + type data = + (int, Bigarray.int16_signed_elt, Bigarray.c_layout) Bigarray.Array1.t array + + let string_of_kind = function `Pcm_s16 -> "pcm_s16" + let copy = copy ~fmt:Bigarray.int16_signed + let make = make ~fmt:Bigarray.int16_signed +end + +include MkContentBase (Specs) + +let kind = lift_kind `Pcm_s16 +let clear = Content_pcm_base.clear_content ~v:0 +let max_int16 = 32767. +let to_value v = int_of_float (v *. max_int16) +let of_value v = float v /. max_int16 + +let from_audio = + Content_pcm_base.from_audio ~to_value ~fmt:Bigarray.int16_signed + +let to_audio = Content_pcm_base.to_audio ~of_value +let blit_audio = Content_pcm_base.blit_audio ~to_value +let channels_of_format = Content_pcm_base.channels_of_format ~get_params diff --git a/src/core/stream/content_pcm_s16.mli b/src/core/stream/content_pcm_s16.mli new file mode 100644 index 0000000000..0c8efa7914 --- /dev/null +++ b/src/core/stream/content_pcm_s16.mli @@ -0,0 +1,36 @@ +(***************************************************************************** + + Liquidsoap, a programmable audio stream generator. + Copyright 2003-2023 Savonet team + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details, fully stated in the COPYING + file at the root of the liquidsoap distribution. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + *****************************************************************************) + +include + Content_base.Content + with type kind = [ `Pcm_s16 ] + and type params = Content_audio.params + and type data = + (int, Bigarray.int16_signed_elt, Bigarray.c_layout) Bigarray.Array1.t + array + +val kind : Content_base.kind +val clear : data -> int -> int -> unit +val from_audio : Content_audio.data -> data +val to_audio : data -> Content_audio.data +val blit_audio : Content_audio.data -> int -> data -> int -> int -> unit +val channels_of_format : Content_base.format -> int diff --git a/src/core/stream/frame_base.ml b/src/core/stream/frame_base.ml index 7bc3c5995c..9263aa9d6a 100644 --- a/src/core/stream/frame_base.ml +++ b/src/core/stream/frame_base.ml @@ -89,3 +89,20 @@ type content_type = Content_base.format Fields.t (** Metadata of a frame. *) type metadata = (string, string) Hashtbl.t + +let audio_format ~pcm_kind params = + let lift_params = + match pcm_kind with + | _ when Content_audio.is_kind pcm_kind -> Content_audio.lift_params + | _ when Content_pcm_s16.is_kind pcm_kind -> Content_pcm_s16.lift_params + | _ when Content_pcm_f32.is_kind pcm_kind -> Content_pcm_f32.lift_params + | _ -> raise Content_base.Invalid + in + lift_params params + +let format_of_channels ~pcm_kind n = + audio_format ~pcm_kind + { + Content_audio.Specs.channel_layout = + lazy (Audio_converter.Channel_layout.layout_of_channels n); + } diff --git a/src/core/stream/mFrame.ml b/src/core/stream/mFrame.ml index 6224e61e62..7b359039bc 100644 --- a/src/core/stream/mFrame.ml +++ b/src/core/stream/mFrame.ml @@ -28,8 +28,11 @@ let mot = midi_of_main let tom = main_of_midi let size () = mot (Lazy.force Frame.size) let position t = mot (position t) -let content b = try Frame.midi b with Not_found -> raise Content.Invalid -let midi b = Content.Midi.get_data (content b) + +let content ?(field = Frame.Fields.midi) b = + try Frame.get b field with Not_found -> raise Content.Invalid + +let midi ?field b = Content.Midi.get_data (content ?field b) let add_break t i = add_break t (tom i) let is_partial = is_partial diff --git a/src/core/stream/mFrame.mli b/src/core/stream/mFrame.mli index 7f366ef35c..4a862f2655 100644 --- a/src/core/stream/mFrame.mli +++ b/src/core/stream/mFrame.mli @@ -22,8 +22,8 @@ val get_all_metadata : t -> (int * metadata) list (** Get the MIDI content. Raises [Not_found] if frame has no midi content. *) -val content : t -> Content.data +val content : ?field:Frame.field -> t -> Content.data (** Get the MIDI content in [Midi] format. Raises [Content.Invalid] * if content is not [Midi] and [Not_found] if frame has no midi content. *) -val midi : t -> Content.Midi.data +val midi : ?field:Frame.field -> t -> Content.Midi.data diff --git a/src/core/stream/vFrame.ml b/src/core/stream/vFrame.ml index da79eb64ab..0cde1e623b 100644 --- a/src/core/stream/vFrame.ml +++ b/src/core/stream/vFrame.ml @@ -31,8 +31,10 @@ let vot ?round x = | None | Some `Down -> Frame.video_of_main x | Some `Up -> Frame.video_of_main (x + Lazy.force Frame.video_rate - 1) -let content b = try Frame.video b with Not_found -> raise Content.Invalid -let data b = Content.Video.get_data (content b) +let content ?(field = Frame.Fields.video) b = + try Frame.get b field with Not_found -> raise Content.Invalid + +let data ?field b = Content.Video.get_data (content ?field b) let size _ = vot (Lazy.force size) let next_sample_position t = vot ~round:`Up (Frame.position t) let add_break t i = add_break t (tov i) diff --git a/src/core/stream/vFrame.mli b/src/core/stream/vFrame.mli index e70cc8c464..5527fe3368 100644 --- a/src/core/stream/vFrame.mli +++ b/src/core/stream/vFrame.mli @@ -58,8 +58,8 @@ val add_break : t -> int -> unit val get_content : Frame.t -> Source.source -> (Content.data * int * int) option (** Get video contents. Raises [Not_found] is frame has no video *) -val content : t -> Content.data +val content : ?field:Frame.field -> t -> Content.data (** Get video content. Raises [Content.Invalid] if video content is not in internal format and [Not_found] if frame has no video content. *) -val data : t -> Content.Video.data +val data : ?field:Frame.field -> t -> Content.Video.data diff --git a/src/core/types/format_type.ml b/src/core/types/format_type.ml index f571924915..0829eed71e 100644 --- a/src/core/types/format_type.ml +++ b/src/core/types/format_type.ml @@ -22,36 +22,61 @@ type Type.custom += Kind of (Content_base.kind * Type.t) type Type.custom += Format of Content_base.format -type Type.constr_t += Track | MuxedTracks | InternalTrack | InternalTracks + +type Type.constr_t += + | PcmAudio + | Track + | MuxedTracks + | InternalTrack + | InternalTracks + type descr = [ `Format of Content_base.format | `Kind of Content_base.kind ] let get_format = function Format f -> f | _ -> assert false let get_kind = function Kind k -> k | _ -> assert false +(* By convention, all format for pcm kind are from Content_audio to + allow shared parameters between the different pcm implementations. *) +let normalize_format f = + match f with + | _ when Content_pcm_s16.is_format f -> + Content_audio.lift_params (Content_pcm_s16.get_params f) + | _ when Content_pcm_f32.is_format f -> + Content_audio.lift_params (Content_pcm_f32.get_params f) + | _ -> f + +let denormalize_format k f = + match k with + | _ when Content_pcm_s16.is_kind k -> + Content_pcm_s16.lift_params (Content_audio.get_params f) + | _ when Content_pcm_f32.is_kind k -> + Content_pcm_f32.lift_params (Content_audio.get_params f) + | _ -> f + let format_handler f = { - Type.typ = Format f; + Type.typ = Format (normalize_format f); copy_with = (fun _ f -> Format (Content_base.duplicate (get_format f))); occur_check = (fun _ _ -> ()); filter_vars = (fun _ l f -> ignore (get_format f); l); - repr = - (fun _ _ f -> `Constr (Content_base.string_of_format (get_format f), [])); + repr = (fun _ _ _ -> assert false); subtype = (fun _ f f' -> Content_base.merge (get_format f) (get_format f')); sup = (fun _ f f' -> Content_base.merge (get_format f) (get_format f'); f); - to_string = (fun f -> Content_base.string_of_format (get_format f)); + to_string = (fun _ -> assert false); } let format_descr f = Type.Custom (format_handler f) let string_of_kind (k, ty) = match (Type.deref ty).Type.descr with - | Type.(Custom { typ = Format f }) -> Content_base.string_of_format f + | Type.(Custom { typ = Format f }) -> + Content_base.string_of_format (denormalize_format k f) | _ -> Printf.sprintf "%s(%s)" (Content_base.string_of_kind k) @@ -60,7 +85,7 @@ let string_of_kind (k, ty) = let repr_of_kind repr l (k, ty) = match (Type.deref ty).Type.descr with | Type.(Custom { typ = Format f }) -> - `Constr (Content_base.string_of_format f, []) + `Constr (Content_base.string_of_format (denormalize_format k f), []) | _ -> `Constr (Content_base.string_of_kind k, [(`Covariant, repr l ty)]) let kind_handler k = @@ -97,7 +122,9 @@ let kind_handler k = let descr descr = let k = match descr with - | `Format f -> (Content_base.kind f, Type.make (format_descr f)) + | `Format f -> + let kind = Content_base.kind f in + (kind, Type.make (format_descr f)) | `Kind k -> (k, Type.var ()) in Type.Custom (kind_handler k) @@ -105,9 +132,7 @@ let descr descr = let rec content_type ?kind ty = match ((Type.demeth ty).Type.descr, kind) with | Type.Custom { Type.typ = Kind (kind, ty) }, None -> content_type ~kind ty - | Type.Custom { Type.typ = Format f }, Some k when Content_base.kind f = k - -> - f + | Type.Custom { Type.typ = Format f }, Some k -> denormalize_format k f | Type.Var _, Some kind -> Content_base.default_format kind | Type.Var _, None -> Runtime_error.raise @@ -125,6 +150,13 @@ module type Content = sig val is_format : Content_base.format -> bool end +let pcm_modules = + [ + (module Content_audio : Content); + (module Content_pcm_s16 : Content); + (module Content_pcm_f32 : Content); + ] + module Content_metadata = struct include Content_timed.Metadata @@ -138,12 +170,12 @@ module Content_track_marks = struct end let internal_modules = - [ - (module Content_audio : Content); - (module Content_video : Content); - (module Content_metadata : Content); - (module Content_track_marks : Content); - ] + pcm_modules + @ [ + (module Content_video : Content); + (module Content_metadata : Content); + (module Content_track_marks : Content); + ] let string_of_kind m = let module Content = (val m : Content) in @@ -157,13 +189,13 @@ let is_format f m = let module Content = (val m : Content) in Content.is_format f -let internal_track = +let check_track ~t modules = { - Type.t = InternalTrack; + Type.t; constr_descr = - Printf.sprintf "an internal track type (%s)" + Printf.sprintf "a track of type: %s" (Utils.concat_with_last ~last:"or" ", " - (List.map string_of_kind internal_modules)); + (List.map string_of_kind modules)); satisfied = (fun ~subtype:_ ~satisfies b -> let b = Type.demeth b in @@ -171,14 +203,17 @@ let internal_track = | Type.Var _ -> satisfies b | Type.(Custom { typ = Ground.Never.Type }) -> () | Type.Custom { Type.typ = Kind (k, _) } - when List.exists (is_kind k) internal_modules -> + when List.exists (is_kind k) modules -> () | Type.Custom { Type.typ = Format f } - when List.exists (is_format f) internal_modules -> + when List.exists (is_kind (Content_base.kind f)) modules -> () | _ -> raise Type.Unsatisfied_constraint); } +let pcm_audio = check_track ~t:PcmAudio pcm_modules +let internal_track = check_track ~t:InternalTrack internal_modules + let internal_tracks = { Type.t = InternalTracks; @@ -248,33 +283,26 @@ let muxed_tracks = } let content_type ty = content_type ty -let audio () = Type.make (descr (`Kind Content_audio.kind)) + +let audio ?(pcm_kind = Content_audio.kind) () = + Type.make (descr (`Kind pcm_kind)) let () = Type.register_type (Content_base.string_of_kind Content_audio.kind) (fun () -> Type.make (Type.Custom (kind_handler (Content_audio.kind, Type.var ())))) -let audio_mono () = - Type.make - (descr - (`Format Content_audio.(lift_params { channel_layout = lazy `Mono }))) - -let audio_stereo () = - Type.make - (descr - (`Format Content_audio.(lift_params { channel_layout = lazy `Stereo }))) - -let audio_n n = +let audio_n ?(pcm_kind = Content_audio.kind) n = Type.make (descr (`Format - Content_audio.( - lift_params - { - channel_layout = - lazy (Audio_converter.Channel_layout.layout_of_channels n); - }))) - + (Frame_base.audio_format ~pcm_kind + { + channel_layout = + lazy (Audio_converter.Channel_layout.layout_of_channels n); + }))) + +let audio_mono ?pcm_kind () = audio_n ?pcm_kind 1 +let audio_stereo ?pcm_kind () = audio_n ?pcm_kind 2 let video () = Type.make (descr (`Kind Content_video.kind)) let () = diff --git a/src/core/types/format_type.mli b/src/core/types/format_type.mli index c65ce72e8e..83aae59f4a 100644 --- a/src/core/types/format_type.mli +++ b/src/core/types/format_type.mli @@ -23,6 +23,7 @@ type descr = [ `Format of Content_base.format | `Kind of Content_base.kind ] val descr : descr -> Type.descr +val pcm_audio : Type.constr val track : Type.constr val muxed_tracks : Type.constr val internal_tracks : Type.constr @@ -30,11 +31,11 @@ val content_type : Type.t -> Content_base.format val kind_handler : Content_base.kind * Type.t -> Type.custom_handler (** Some common types *) -val audio : unit -> Type.t +val audio : ?pcm_kind:Content_base.kind -> unit -> Type.t -val audio_mono : unit -> Type.t -val audio_stereo : unit -> Type.t -val audio_n : int -> Type.t +val audio_mono : ?pcm_kind:Content_base.kind -> unit -> Type.t +val audio_stereo : ?pcm_kind:Content_base.kind -> unit -> Type.t +val audio_n : ?pcm_kind:Content_base.kind -> int -> Type.t val video : unit -> Type.t val midi : unit -> Type.t val midi_n : int -> Type.t diff --git a/src/core/types/frame_type.ml b/src/core/types/frame_type.ml index 31059fb7ff..ab1a8c46af 100644 --- a/src/core/types/frame_type.ml +++ b/src/core/types/frame_type.ml @@ -41,6 +41,8 @@ let make ?pos base_type fields = let internal_tracks ?pos () = Type.var ?pos ~constraints:[Format_type.internal_tracks] () +let pcm_audio ?pos () = Type.var ?pos ~constraints:[Format_type.pcm_audio] () + let set_field frame_type field field_type = let field = Frame.Fields.string_of_field field in let meth = @@ -78,7 +80,9 @@ let content_type frame_type = | [], Type.Var _ -> let audio = if Frame_settings.conf_audio_channels#get > 0 then - Some (Format_type.audio_n Frame_settings.conf_audio_channels#get) + Some + (Format_type.audio_n ~pcm_kind:Content_audio.kind + Frame_settings.conf_audio_channels#get) else None in let video = @@ -99,7 +103,8 @@ let content_type frame_type = List.iter (function | { Type.meth = "audio"; scheme = [], ty } -> - Typing.(ty <: Format_type.audio ()) + Typing.( + ty <: Format_type.audio ~pcm_kind:Content_audio.kind ()) | { Type.meth = "video"; scheme = [], ty } -> Typing.(ty <: Format_type.video ()) | _ -> ()) diff --git a/src/core/types/frame_type.mli b/src/core/types/frame_type.mli index 7862ba4140..3559ba5d6a 100644 --- a/src/core/types/frame_type.mli +++ b/src/core/types/frame_type.mli @@ -31,6 +31,9 @@ val make : ?pos:Pos.t -> Type.t -> Type.t Frame.Fields.t -> Type.t (* Same as [Lang.internal_tracks_t] (with position) *) val internal_tracks : ?pos:Pos.t -> unit -> Type.t +(* Same as [Lang.pcm_audio_t] (with position) *) +val pcm_audio : ?pos:Pos.t -> unit -> Type.t + (* [set_field frame_type field field_type] assigns a field to a frame type. *) val set_field : Type.t -> Frame.field -> Type.t -> Type.t diff --git a/src/libs/audio.liq b/src/libs/audio.liq index 4a597f5da7..c2cfde9d28 100644 --- a/src/libs/audio.liq +++ b/src/libs/audio.liq @@ -1,5 +1,37 @@ audio = () +let audio.encode = () + +# Encode audio track to pcm_s16 +# @category Source / Audio processing +def audio.encode.pcm_s16(~id=null("audio.encode.pcm_s16"), s) = + let {audio, ...tracks} = source.tracks(s) + source(id=id, tracks.{audio = track.encode.audio.pcm_s16(audio)}) +end + +# Encode audio track to pcm_f32 +# @category Source / Audio processing +def audio.encode.pcm_f32(~id=null("audio.encode.pcm_f32"), s) = + let {audio, ...tracks} = source.tracks(s) + source(id=id, tracks.{audio = track.encode.audio.pcm_f32(audio)}) +end + +let audio.decode = () + +# Decode audio track to pcm_s16 +# @category Source / Audio processing +def audio.decode.pcm_s16(~id=null("audio.decode.pcm_s16"), s) = + let {audio, ...tracks} = source.tracks(s) + source(id=id, tracks.{audio = track.decode.audio.pcm_s16(audio)}) +end + +# Decode audio track to pcm_f32 +# @category Source / Audio processing +def audio.decode.pcm_f32(~id=null("audio.decode.pcm_f32"), s) = + let {audio, ...tracks} = source.tracks(s) + source(id=id, tracks.{audio = track.decode.audio.pcm_f32(audio)}) +end + # Samplerate for audio. # @category Settings def audio.samplerate = diff --git a/tests/language/encoders.liq b/tests/language/encoders.liq index 272c223fc5..1fa4c6dc90 100755 --- a/tests/language/encoders.liq +++ b/tests/language/encoders.liq @@ -7,6 +7,47 @@ def f() = end end + + try + let eval _ = "n = false + ignore(%mp3(stereo=n))" + test.fail() + catch _ do + () + end + + try + let eval _ = "n = false + ignore(%mp3(mono=n))" + test.fail() + catch _ do + () + end + + try + let eval _ = "n = 2 + ignore(%mp3(channels=n))" + test.fail() + catch _ do + () + end + + try + let eval _ = "n = 2 + ignore(%ffmpeg(%audio(channels=n)))" + test.fail() + catch _ do + () + end + + try + let eval _ = "n = 2 + ignore(%ffmpeg(%audio(ac=n)))" + test.fail() + catch _ do + () + end + test.pass() end diff --git a/tests/media/dune.inc b/tests/media/dune.inc index c0ee2bf039..d9949c4b59 100644 --- a/tests/media/dune.inc +++ b/tests/media/dune.inc @@ -169,6 +169,26 @@ (action (with-stdout-to %{target} (run %{mk_encoder_test} "%ffmpeg(format=\"mp4\",%audio(codec=\"aac\"))" sine "@ffmpeg[format='mp4',@audio[codec='aac']].mp4")))) +(rule + (alias citest) + (package liquidsoap) + (target @ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']]_encoder.liq) + (deps + (:mk_encoder_test ./mk_encoder_test.sh) + (:test_encoder_in ./test_encoder.liq.in)) + (action + (with-stdout-to %{target} + (run %{mk_encoder_test} "%ffmpeg(format=\"mp4\",%audio(pcm_s16,codec=\"aac\"))" sine "@ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']].mp4")))) +(rule + (alias citest) + (package liquidsoap) + (target @ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']]_encoder.liq) + (deps + (:mk_encoder_test ./mk_encoder_test.sh) + (:test_encoder_in ./test_encoder.liq.in)) + (action + (with-stdout-to %{target} + (run %{mk_encoder_test} "%ffmpeg(format=\"mp4\",%audio(pcm_f32,codec=\"aac\"))" sine "@ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']].mp4")))) (rule (alias citest) (package liquidsoap) @@ -440,6 +460,32 @@ (:run_test ../run_test.exe)) (action (run %{run_test} %{encoder} liquidsoap %{test_liq} %{encoder} -- "%ffmpeg(format=\"mp4\",%audio(codec=\"aac\"))"))) +(rule + (alias citest) + (package liquidsoap) + (target @ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']].mp4) + (deps + (:encoder @ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']]_encoder.liq) + (package liquidsoap) + ../../src/bin/liquidsoap.exe + (:stdlib ../../src/libs/stdlib.liq) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action + (run %{run_test} %{encoder} liquidsoap %{test_liq} %{encoder} -- "%ffmpeg(format=\"mp4\",%audio(pcm_s16,codec=\"aac\"))"))) +(rule + (alias citest) + (package liquidsoap) + (target @ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']].mp4) + (deps + (:encoder @ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']]_encoder.liq) + (package liquidsoap) + ../../src/bin/liquidsoap.exe + (:stdlib ../../src/libs/stdlib.liq) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action + (run %{run_test} %{encoder} liquidsoap %{test_liq} %{encoder} -- "%ffmpeg(format=\"mp4\",%audio(pcm_f32,codec=\"aac\"))"))) (rule (alias citest) (package liquidsoap) @@ -527,6 +573,8 @@ @ogg[@opus[mono]].ogg @ogg[@opus[stereo]].ogg @ffmpeg[format='mp4',@audio[codec='aac']].mp4 +@ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']].mp4 +@ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']].mp4 @ffmpeg[format='mp4',@audio[codec='aac',channels=1],@video[codec='libx264']].mp4 @ffmpeg[format='mp4',@audio[codec='aac',channels=2],@video[codec='libx264']].mp4 @ffmpeg[format='mp4',@audio[codec='aac',channels=2],@audio_2[codec='aac',channels=1],@video[codec='libx264'],@video_2[codec='libx264']].mp4 @@ -1196,6 +1244,84 @@ (:run_test ../run_test.exe)) (action (run %{run_test} "FFmpeg audio decoder test for @ffmpeg[format='mp4',@audio[codec='aac']].mp4" liquidsoap %{test_liq} test_ffmpeg_audio_decoder.liq -- "@ffmpeg[format='mp4',@audio[codec='aac']].mp4"))) +(rule + (alias citest) + (package liquidsoap) + (deps + all_media_files + test_mono.liq + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:stdlib ../../src/libs/stdlib.liq) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action + (run %{run_test} "Mono decoding test for @ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']].mp4" liquidsoap %{test_liq} test_mono.liq -- "@ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']].mp4"))) +(rule + (alias citest) + (package liquidsoap) + (deps + all_media_files + test_stereo.liq + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:stdlib ../../src/libs/stdlib.liq) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action + (run %{run_test} "Stereo decoding test for @ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']].mp4" liquidsoap %{test_liq} test_stereo.liq -- "@ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']].mp4"))) +(rule + (alias citest) + (package liquidsoap) + (deps + all_media_files + test_ffmpeg_audio_decoder.liq + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:stdlib ../../src/libs/stdlib.liq) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action + (run %{run_test} "FFmpeg audio decoder test for @ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']].mp4" liquidsoap %{test_liq} test_ffmpeg_audio_decoder.liq -- "@ffmpeg[format='mp4',@audio[pcm_s16,codec='aac']].mp4"))) +(rule + (alias citest) + (package liquidsoap) + (deps + all_media_files + test_mono.liq + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:stdlib ../../src/libs/stdlib.liq) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action + (run %{run_test} "Mono decoding test for @ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']].mp4" liquidsoap %{test_liq} test_mono.liq -- "@ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']].mp4"))) +(rule + (alias citest) + (package liquidsoap) + (deps + all_media_files + test_stereo.liq + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:stdlib ../../src/libs/stdlib.liq) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action + (run %{run_test} "Stereo decoding test for @ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']].mp4" liquidsoap %{test_liq} test_stereo.liq -- "@ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']].mp4"))) +(rule + (alias citest) + (package liquidsoap) + (deps + all_media_files + test_ffmpeg_audio_decoder.liq + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:stdlib ../../src/libs/stdlib.liq) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action + (run %{run_test} "FFmpeg audio decoder test for @ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']].mp4" liquidsoap %{test_liq} test_ffmpeg_audio_decoder.liq -- "@ffmpeg[format='mp4',@audio[pcm_f32,codec='aac']].mp4"))) (rule (alias citest) (package liquidsoap) @@ -1781,3 +1907,16 @@ (:run_test ../run_test.exe)) (action (run %{run_test} "test_taglib.liq" liquidsoap %{test_liq} test_taglib.liq -- ""))) +(rule + (alias citest) + (package liquidsoap) + (deps + all_media_files + test_pcm_s16_decode.liq + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:stdlib ../../src/libs/stdlib.liq) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action + (run %{run_test} "test_pcm_s16_decode.liq" liquidsoap %{test_liq} test_pcm_s16_decode.liq -- ""))) diff --git a/tests/media/gen_dune.ml b/tests/media/gen_dune.ml index a33dd91189..5445a21377 100644 --- a/tests/media/gen_dune.ml +++ b/tests/media/gen_dune.ml @@ -31,6 +31,7 @@ let standalone_tests = "test_ffmpeg_distributed_hls.liq"; "test_ffmpeg_raw_hls.liq"; "test_taglib.liq"; + "test_pcm_s16_decode.liq"; ] let audio_formats = @@ -52,6 +53,8 @@ let audio_formats = "%ogg(%opus(mono)).ogg"; "%ogg(%opus(stereo)).ogg"; {|%ffmpeg(format="mp4",%audio(codec="aac")).mp4|}; + {|%ffmpeg(format="mp4",%audio(pcm_s16,codec="aac")).mp4|}; + {|%ffmpeg(format="mp4",%audio(pcm_f32,codec="aac")).mp4|}; ] let video_formats = [{|%ffmpeg(format="mp4",%video(codec="libx264")).mp4|}] diff --git a/tests/media/test_pcm_f32_decode.liq b/tests/media/test_pcm_f32_decode.liq new file mode 100755 index 0000000000..595d9ca07c --- /dev/null +++ b/tests/media/test_pcm_f32_decode.liq @@ -0,0 +1,7 @@ +s = once(single("@mp3[mono].mp3")) + +s = (s:source(audio=pcm_f32(mono))) + +s = source.on_track(s, fun (_) -> test.pass()) + +output.dummy(fallible=true, s) diff --git a/tests/media/test_pcm_s16_decode.liq b/tests/media/test_pcm_s16_decode.liq new file mode 100755 index 0000000000..5dee8794ea --- /dev/null +++ b/tests/media/test_pcm_s16_decode.liq @@ -0,0 +1,7 @@ +s = once(single("@mp3[mono].mp3")) + +s = (s:source(audio=pcm_s16(mono))) + +s = source.on_track(s, fun (_) -> test.pass()) + +output.dummy(fallible=true, s) diff --git a/tests/regression/dune.inc b/tests/regression/dune.inc index f3522b3396..b85cd22356 100644 --- a/tests/regression/dune.inc +++ b/tests/regression/dune.inc @@ -142,6 +142,19 @@ (:run_test ../run_test.exe)) (action (run %{run_test} initial_request_queue.liq liquidsoap %{test_liq} initial_request_queue.liq))) +(rule + (alias citest) + (package liquidsoap) + (deps + unified-pcm-types.liq + ../media/all_media_files + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:stdlib ../../src/libs/stdlib.liq) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action (run %{run_test} unified-pcm-types.liq liquidsoap %{test_liq} unified-pcm-types.liq))) + (rule (alias citest) (package liquidsoap) diff --git a/tests/regression/unified-pcm-types.liq b/tests/regression/unified-pcm-types.liq new file mode 100644 index 0000000000..e74cede099 --- /dev/null +++ b/tests/regression/unified-pcm-types.liq @@ -0,0 +1,19 @@ +# Type parameters below are shared between pcm_s16 source and the decoded +# source. We want to make sure that this works at runtime. +def f() = + s = blank() + + s = source.on_track(s, fun (_) -> test.pass()) + + s = audio.decode.pcm_s16(s) + + output.file( + %ffmpeg( + %audio(codec="aac") + ), + "unified-pcm-types.aac", + s + ) +end + +test.check(f) diff --git a/tests/streams/dune.inc b/tests/streams/dune.inc index dd079cd4e6..028a592458 100644 --- a/tests/streams/dune.inc +++ b/tests/streams/dune.inc @@ -22,6 +22,29 @@ (:run_test ../run_test.exe)) (action (run %{run_test} 197.liq liquidsoap %{test_liq} 197.liq))) +(rule + (alias citest) + (package liquidsoap) + (deps + sine.detect.pcm_f32.liq + ./file1.mp3 + ./file2.mp3 + ./file3.mp3 + ./jingle1.mp3 + ./jingle2.mp3 + ./jingle3.mp3 + ./file1.png + ./file2.png + ./jingles + ./playlist + ./huge_playlist + ../media/all_media_files + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action (run %{run_test} sine.detect.pcm_f32.liq liquidsoap %{test_liq} sine.detect.pcm_f32.liq))) + (rule (alias citest) (package liquidsoap) @@ -45,6 +68,29 @@ (:run_test ../run_test.exe)) (action (run %{run_test} huge-playlist.liq liquidsoap %{test_liq} huge-playlist.liq))) +(rule + (alias citest) + (package liquidsoap) + (deps + sine.detect.pcm_s16.liq + ./file1.mp3 + ./file2.mp3 + ./file3.mp3 + ./jingle1.mp3 + ./jingle2.mp3 + ./jingle3.mp3 + ./file1.png + ./file2.png + ./jingles + ./playlist + ./huge_playlist + ../media/all_media_files + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action (run %{run_test} sine.detect.pcm_s16.liq liquidsoap %{test_liq} sine.detect.pcm_s16.liq))) + (rule (alias citest) (package liquidsoap) @@ -689,6 +735,29 @@ (:run_test ../run_test.exe)) (action (run %{run_test} srt_multiple_outputs.liq liquidsoap %{test_liq} srt_multiple_outputs.liq))) +(rule + (alias citest) + (package liquidsoap) + (deps + sine.detect.full_conv.liq + ./file1.mp3 + ./file2.mp3 + ./file3.mp3 + ./jingle1.mp3 + ./jingle2.mp3 + ./jingle3.mp3 + ./file1.png + ./file2.png + ./jingles + ./playlist + ./huge_playlist + ../media/all_media_files + ../../src/bin/liquidsoap.exe + (package liquidsoap) + (:test_liq ../test.liq) + (:run_test ../run_test.exe)) + (action (run %{run_test} sine.detect.full_conv.liq liquidsoap %{test_liq} sine.detect.full_conv.liq))) + (rule (alias citest) (package liquidsoap) diff --git a/tests/streams/sine.detect.full_conv.liq b/tests/streams/sine.detect.full_conv.liq new file mode 100755 index 0000000000..628a687de9 --- /dev/null +++ b/tests/streams/sine.detect.full_conv.liq @@ -0,0 +1,16 @@ +log.level.set(4) + +def f(freq) + print("Detected sine at #{freq}Hz.") + test.pass() + shutdown() +end + +s = sine(440.) +s = (s:source(audio=pcm)) +s = audio.encode.pcm_s16(s) +s = audio.decode.pcm_s16(s) +s = audio.encode.pcm_f32(s) +s = audio.decode.pcm_f32(s) +s = sine.detect(debug=false, [440.], s, f) +output.dummy(s) diff --git a/tests/streams/sine.detect.pcm_f32.liq b/tests/streams/sine.detect.pcm_f32.liq new file mode 100755 index 0000000000..1e94ed50c2 --- /dev/null +++ b/tests/streams/sine.detect.pcm_f32.liq @@ -0,0 +1,13 @@ +log.level.set(4) + +def f(freq) + print("Detected sine at #{freq}Hz.") + test.pass() + shutdown() +end + +s = sine(440.) +s = (s:source(audio=pcm_s16)) +s = audio.decode.pcm_s16(s) +s = sine.detect(debug=false, [440.], s, f) +output.dummy(s) diff --git a/tests/streams/sine.detect.pcm_s16.liq b/tests/streams/sine.detect.pcm_s16.liq new file mode 100755 index 0000000000..1e94ed50c2 --- /dev/null +++ b/tests/streams/sine.detect.pcm_s16.liq @@ -0,0 +1,13 @@ +log.level.set(4) + +def f(freq) + print("Detected sine at #{freq}Hz.") + test.pass() + shutdown() +end + +s = sine(440.) +s = (s:source(audio=pcm_s16)) +s = audio.decode.pcm_s16(s) +s = sine.detect(debug=false, [440.], s, f) +output.dummy(s)