summaryrefslogtreecommitdiff
path: root/Userland/Utilities/aplay.cpp
blob: 1a9a1d188564ab1317a720b18c085d1f5604aec2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/*
 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
 * Copyright (c) 2021-2022, kleines Filmröllchen <filmroellchen@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/Types.h>
#include <LibAudio/ConnectionToServer.h>
#include <LibAudio/Loader.h>
#include <LibAudio/Resampler.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/EventLoop.h>
#include <LibCore/File.h>
#include <LibCore/System.h>
#include <LibMain/Main.h>
#include <math.h>
#include <stdio.h>

// The Kernel has issues with very large anonymous buffers.
// FIXME: This appears to be fine for now, but it's really a hack.
constexpr size_t LOAD_CHUNK_SIZE = 128 * KiB;

ErrorOr<int> serenity_main(Main::Arguments arguments)
{
    TRY(Core::System::pledge("stdio rpath sendfd unix thread"));

    StringView path {};
    bool should_loop = false;
    bool show_sample_progress = false;

    Core::ArgsParser args_parser;
    args_parser.add_positional_argument(path, "Path to audio file", "path");
    args_parser.add_option(should_loop, "Loop playback", "loop", 'l');
    args_parser.add_option(show_sample_progress, "Show playback progress in samples", "sample-progress", 's');
    args_parser.parse(arguments);

    TRY(Core::System::unveil("/sys/kernel/processes", "r"));
    TRY(Core::System::unveil("/tmp/session/%sid/portal/audio", "rw"));
    TRY(Core::System::unveil(Core::File::absolute_path(path), "r"sv));
    TRY(Core::System::unveil(nullptr, nullptr));

    Core::EventLoop loop;

    auto audio_client = TRY(Audio::ConnectionToServer::try_create());
    auto maybe_loader = Audio::Loader::create(path);
    if (maybe_loader.is_error()) {
        warnln("Failed to load audio file: {}", maybe_loader.error().description);
        return 1;
    }
    auto loader = maybe_loader.release_value();

    TRY(Core::System::pledge("stdio sendfd thread"));

    outln("\033[34;1m Playing\033[0m: {}", path);
    outln("\033[34;1m  Format\033[0m: {} {} Hz, {}-bit, {}",
        loader->format_name(),
        loader->sample_rate(),
        loader->bits_per_sample(),
        loader->num_channels() == 1 ? "Mono" : "Stereo");
    out("\033[34;1mProgress\033[0m: \033[s");

    auto resampler = Audio::ResampleHelper<Audio::Sample>(loader->sample_rate(), audio_client->get_sample_rate());

    // If we're downsampling, we need to appropriately load more samples at once.
    size_t const load_size = static_cast<size_t>(LOAD_CHUNK_SIZE * static_cast<double>(loader->sample_rate()) / static_cast<double>(audio_client->get_sample_rate()));
    // We assume that the loader can load samples at at least 2x speed (testing confirms 9x-12x for FLAC, 14x for WAV).
    // Therefore, when the server-side buffer can only play as long as the time it takes us to load a chunk,
    // we give it new data.
    unsigned const min_buffer_size = load_size / 2;

    auto print_playback_update = [&]() {
        out("\033[u");
        if (show_sample_progress) {
            out("{}/{}", audio_client->total_played_samples(), loader->total_samples());
        } else {
            auto playing_seconds = static_cast<int>(floor(static_cast<double>(audio_client->total_played_samples()) / static_cast<double>(loader->sample_rate())));
            auto playing_minutes = playing_seconds / 60;
            auto playing_seconds_of_minute = playing_seconds % 60;

            auto total_seconds = static_cast<int>(floor(static_cast<double>(loader->total_samples()) / static_cast<double>(loader->sample_rate())));
            auto total_minutes = total_seconds / 60;
            auto total_seconds_of_minute = total_seconds % 60;

            auto remaining_seconds = total_seconds - playing_seconds;
            auto remaining_minutes = remaining_seconds / 60;
            auto remaining_seconds_of_minute = remaining_seconds % 60;

            out("\033[1m{:02d}:{:02d}\033[0m [{}{:02d}:{:02d}] -- {:02d}:{:02d}",
                playing_minutes, playing_seconds_of_minute,
                remaining_seconds == 0 ? " " : "-",
                remaining_minutes, remaining_seconds_of_minute,
                total_minutes, total_seconds_of_minute);
        }
        fflush(stdout);
    };

    for (;;) {
        auto samples = loader->get_more_samples(load_size);
        if (!samples.is_error()) {
            if (samples.value().size() > 0) {
                print_playback_update();
                // We can read and enqueue more samples
                resampler.reset();
                auto resampled_samples = resampler.resample(move(samples.value()));
                TRY(audio_client->async_enqueue(move(resampled_samples)));
            } else if (should_loop) {
                // We're done: now loop
                auto result = loader->reset();
                if (result.is_error()) {
                    outln();
                    outln("Error while resetting: {} (at {:x})", result.error().description, result.error().index);
                }
            } else if (samples.value().size() == 0 && audio_client->remaining_samples() == 0) {
                // We're done and the server is done
                break;
            }
            while (audio_client->remaining_samples() > min_buffer_size) {
                // The server has enough data for now
                print_playback_update();
                usleep(1'000'000 / 10);
            }
        } else {
            outln();
            outln("Error: {} (at {:x})", samples.error().description, samples.error().index);
            return 1;
        }
    }
    outln();
    return 0;
}