2023-03-08 14:27:03 +00:00
/*
* Copyright ( c ) 2023 , kleines Filmröllchen < filmroellchen @ serenityos . org >
*
* SPDX - License - Identifier : BSD - 2 - Clause
*/
# include <AK/LexicalPath.h>
# include <AK/Types.h>
2023-07-04 22:12:08 +00:00
# include <LibAudio/Encoder.h>
# include <LibAudio/FlacWriter.h>
2023-03-08 14:27:03 +00:00
# include <LibAudio/Loader.h>
# include <LibAudio/WavWriter.h>
# include <LibCore/ArgsParser.h>
# include <LibCore/System.h>
2023-06-22 19:53:00 +00:00
# include <LibFileSystem/FileSystem.h>
2023-03-08 14:27:03 +00:00
# include <LibMain/Main.h>
# include <stdio.h>
static ErrorOr < StringView > guess_format_from_extension ( StringView path )
{
if ( path = = " - " sv )
return Error : : from_string_view ( " Cannot guess format for standard stream, please specify format manually " sv ) ;
LexicalPath lexical_path { path } ;
auto extension = lexical_path . extension ( ) ;
if ( extension . is_empty ( ) )
return Error : : from_string_view ( " Cannot guess format for file without file extension " sv ) ;
// Note: Do not return the `extension` StringView in any case, since that will possibly lead to UAF.
if ( extension = = " wav " sv | | extension = = " wave " sv )
return " wav " sv ;
if ( extension = = " flac " sv )
return " flac " sv ;
if ( extension = = " mp3 " sv | | extension = = " mpeg3 " sv )
return " mp3 " sv ;
if ( extension = = " qoa " sv )
return " qoa " sv ;
return Error : : from_string_view ( " Cannot guess format for the given file extension " sv ) ;
}
static ErrorOr < Audio : : PcmSampleFormat > parse_sample_format ( StringView textual_format )
{
if ( textual_format = = " u8 " sv )
return Audio : : PcmSampleFormat : : Uint8 ;
if ( textual_format = = " s16le " sv )
return Audio : : PcmSampleFormat : : Int16 ;
if ( textual_format = = " s24le " sv )
return Audio : : PcmSampleFormat : : Int24 ;
if ( textual_format = = " s32le " sv )
return Audio : : PcmSampleFormat : : Int32 ;
if ( textual_format = = " f32le " sv )
return Audio : : PcmSampleFormat : : Float32 ;
if ( textual_format = = " f64le " sv )
return Audio : : PcmSampleFormat : : Float64 ;
return Error : : from_string_view ( " Unknown sample format " sv ) ;
}
ErrorOr < int > serenity_main ( Main : : Arguments arguments )
{
TRY ( Core : : System : : pledge ( " stdio rpath wpath cpath " ) ) ;
StringView input { } ;
StringView output { } ;
StringView input_format { } ;
StringView output_format { } ;
StringView output_sample_format ;
Core : : ArgsParser args_parser ;
args_parser . set_general_help ( " Convert between audio formats " ) ;
args_parser . add_option ( input , " Audio file to convert (or '-' for standard input) " , " input " , ' i ' , " input " ) ;
args_parser . add_option ( input_format , " Force input codec and container (see manual for supported codecs and containers) " , " input-audio-codec " , 0 , " input-codec " ) ;
args_parser . add_option ( output_format , " Set output codec " , " audio-codec " , 0 , " output-codec " ) ;
args_parser . add_option ( output_sample_format , " Set output sample format (see manual for supported formats) " , " audio-format " , 0 , " sample-format " ) ;
args_parser . add_option ( output , " Target file (or '-' for standard output) " , " output " , ' o ' , " output " ) ;
args_parser . parse ( arguments ) ;
if ( input . is_empty ( ) )
return Error : : from_string_view ( " Input file is required, use '-' to read from standard input " sv ) ;
if ( output_format . is_empty ( ) & & output = = " - " sv )
return Error : : from_string_view ( " Output format must be specified manually when writing to standard output " sv ) ;
if ( input ! = " - " sv )
2023-06-22 19:53:00 +00:00
TRY ( Core : : System : : unveil ( TRY ( FileSystem : : absolute_path ( input ) ) , " r " sv ) ) ;
2023-03-08 14:27:03 +00:00
if ( output ! = " - " sv )
2023-06-22 19:53:00 +00:00
TRY ( Core : : System : : unveil ( TRY ( FileSystem : : absolute_path ( output ) ) , " rwc " sv ) ) ;
2023-03-08 14:27:03 +00:00
TRY ( Core : : System : : unveil ( nullptr , nullptr ) ) ;
RefPtr < Audio : : Loader > input_loader ;
// Use normal loader infrastructure to guess input format.
if ( input_format . is_empty ( ) ) {
auto loader_or_error = Audio : : Loader : : create ( input ) ;
if ( loader_or_error . is_error ( ) ) {
2023-07-04 22:12:08 +00:00
warnln ( " Could not guess codec for input file '{}'. Try forcing a codec with '--input-audio-codec' " , input ) ;
2023-03-08 14:27:03 +00:00
return 1 ;
}
input_loader = loader_or_error . release_value ( ) ;
} else {
warnln ( " Forcing input codec is not supported " ) ;
return 1 ;
}
VERIFY ( input_loader ) ;
if ( output_format . is_empty ( ) )
output_format = TRY ( guess_format_from_extension ( output ) ) ;
VERIFY ( ! output_format . is_empty ( ) ) ;
2023-07-04 22:12:08 +00:00
Optional < NonnullOwnPtr < Audio : : Encoder > > writer ;
if ( ! output . is_empty ( ) ) {
if ( output_format = = " wav " sv ) {
2023-03-08 14:27:03 +00:00
auto parsed_output_sample_format = input_loader - > pcm_format ( ) ;
if ( ! output_sample_format . is_empty ( ) )
parsed_output_sample_format = TRY ( parse_sample_format ( output_sample_format ) ) ;
writer . emplace ( TRY ( Audio : : WavWriter : : create_from_file (
output ,
static_cast < int > ( input_loader - > sample_rate ( ) ) ,
input_loader - > num_channels ( ) ,
parsed_output_sample_format ) ) ) ;
2023-07-04 22:12:08 +00:00
} else if ( output_format = = " flac " sv ) {
auto parsed_output_sample_format = input_loader - > pcm_format ( ) ;
if ( ! output_sample_format . is_empty ( ) )
parsed_output_sample_format = TRY ( parse_sample_format ( output_sample_format ) ) ;
if ( ! Audio : : is_integer_format ( parsed_output_sample_format ) ) {
warnln ( " FLAC does not support sample format {} " , Audio : : sample_format_name ( parsed_output_sample_format ) ) ;
return 1 ;
}
auto output_stream = TRY ( Core : : OutputBufferedFile : : create ( TRY ( Core : : File : : open ( output , Core : : File : : OpenMode : : Write | Core : : File : : OpenMode : : Truncate ) ) ) ) ;
auto flac_writer = TRY ( Audio : : FlacWriter : : create (
move ( output_stream ) ,
static_cast < int > ( input_loader - > sample_rate ( ) ) ,
input_loader - > num_channels ( ) ,
Audio : : pcm_bits_per_sample ( parsed_output_sample_format ) ) ) ;
TRY ( flac_writer - > finalize_header_format ( ) ) ;
writer . emplace ( move ( flac_writer ) ) ;
} else {
warnln ( " Codec {} is not supported for encoding " , output_format ) ;
return 1 ;
2023-03-08 14:27:03 +00:00
}
2023-07-04 22:12:08 +00:00
2023-03-08 14:27:03 +00:00
if ( output ! = " - " sv )
out ( " Writing: \033 [s " ) ;
2023-07-04 22:12:53 +00:00
auto start = MonotonicTime : : now ( ) ;
2023-03-08 14:27:03 +00:00
while ( input_loader - > loaded_samples ( ) < input_loader - > total_samples ( ) ) {
auto samples_or_error = input_loader - > get_more_samples ( ) ;
if ( samples_or_error . is_error ( ) ) {
warnln ( " Error while loading samples: {} (at {}) " , samples_or_error . error ( ) . description , samples_or_error . error ( ) . index ) ;
return 1 ;
}
auto samples = samples_or_error . release_value ( ) ;
if ( writer . has_value ( ) )
TRY ( ( * writer ) - > write_samples ( samples . span ( ) ) ) ;
// TODO: Show progress updates like aplay by moving the progress calculation into a common utility function.
if ( output ! = " - " sv ) {
out ( " \033 [u{}/{} " , input_loader - > loaded_samples ( ) , input_loader - > total_samples ( ) ) ;
fflush ( stdout ) ;
}
}
2023-07-04 22:12:53 +00:00
auto end = MonotonicTime : : now ( ) ;
auto seconds_to_write = ( end - start ) . to_milliseconds ( ) / 1000.0 ;
dbgln ( " Wrote {} samples in {:.3f}s, {:3.2f}% realtime " , input_loader - > loaded_samples ( ) , seconds_to_write , input_loader - > loaded_samples ( ) / static_cast < double > ( input_loader - > sample_rate ( ) ) / seconds_to_write * 100.0 ) ;
2023-03-08 14:27:03 +00:00
if ( writer . has_value ( ) )
2023-06-30 18:02:49 +00:00
TRY ( ( * writer ) - > finalize ( ) ) ;
2023-03-08 14:27:03 +00:00
if ( output ! = " - " sv )
outln ( ) ;
}
return 0 ;
}