/* * Copyright (c) 2023, kleines Filmröllchen * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include static ErrorOr 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 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 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) TRY(Core::System::unveil(TRY(FileSystem::absolute_path(input)), "r"sv)); if (output != "-"sv) TRY(Core::System::unveil(TRY(FileSystem::absolute_path(output)), "rwc"sv)); TRY(Core::System::unveil(nullptr, nullptr)); RefPtr 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()) { warnln("Could not guess codec for input file '{}'. Try forcing a codec with '--input-audio-codec'", input); 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()); Optional> writer; if (!output.is_empty()) { if (output_format == "wav"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)); writer.emplace(TRY(Audio::WavWriter::create_from_file( output, static_cast(input_loader->sample_rate()), input_loader->num_channels(), parsed_output_sample_format))); } 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(input_loader->sample_rate()), input_loader->num_channels(), Audio::pcm_bits_per_sample(parsed_output_sample_format))); writer.emplace(move(flac_writer)); } else { warnln("Codec {} is not supported for encoding", output_format); return 1; } if (writer.has_value()) { (*writer)->sample_count_hint(input_loader->total_samples()); auto metadata = input_loader->metadata(); metadata.replace_encoder_with_serenity(); TRY((*writer)->set_metadata(metadata)); } // FIXME: Maybe use a generalized interface for this as well if the need arises. if (output_format == "flac"sv) TRY(static_cast(writer->ptr())->finalize_header_format()); if (output != "-"sv) out("Writing: \033[s"); auto start = MonotonicTime::now(); 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); } } 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(input_loader->sample_rate()) / seconds_to_write * 100.0); if (writer.has_value()) TRY((*writer)->finalize()); if (output != "-"sv) outln(); } return 0; }