Coverage Report

Created: 2026-07-25 07:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibAudio/Sample.h
Line
Count
Source
1
/*
2
 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3
 * Copyright (c) 2021, kleines Filmröllchen <filmroellchen@serenityos.org>
4
 *
5
 * SPDX-License-Identifier: BSD-2-Clause
6
 */
7
8
#pragma once
9
10
#include <AK/Format.h>
11
#include <AK/Math/Constants.h>
12
#include <AK/Math/Exponentials.h>
13
#include <AK/Math/Sqrt.h>
14
#include <AK/Math/Trigonometry.h>
15
16
namespace Audio {
17
using AK::Exponentials::exp;
18
using AK::Exponentials::log;
19
// Constants for logarithmic volume. See Sample::linear_to_log
20
// Corresponds to 60dB
21
constexpr float DYNAMIC_RANGE = 1000;
22
constexpr float VOLUME_A = 1 / DYNAMIC_RANGE;
23
float const VOLUME_B = log(DYNAMIC_RANGE);
24
25
// A single sample in an audio buffer.
26
// Values are floating point, and should range from -1.0 to +1.0
27
struct Sample {
28
1.41G
    constexpr Sample() = default;
29
30
    // For mono
31
    constexpr explicit Sample(float left)
32
33.8M
        : left(left)
33
33.8M
        , right(left)
34
33.8M
    {
35
33.8M
    }
36
37
    // For stereo
38
    constexpr Sample(float left, float right)
39
75.7M
        : left(left)
40
75.7M
        , right(right)
41
75.7M
    {
42
75.7M
    }
43
44
    // Returns the absolute maximum range (separate per channel) of the given sample buffer.
45
    // For example { 0.8, 0 } means that samples on the left channel occupy the range { -0.8, 0.8 },
46
    // while all samples on the right channel are 0.
47
    static Sample max_range(ReadonlySpan<Sample> span)
48
0
    {
49
0
        Sample result { NumericLimits<float>::min_normal(), NumericLimits<float>::min_normal() };
50
0
        for (Sample sample : span) {
51
0
            result.left = max(result.left, AK::fabs(sample.left));
52
0
            result.right = max(result.right, AK::fabs(sample.right));
53
0
        }
54
0
        return result;
55
0
    }
56
57
    void clip()
58
0
    {
59
0
        left = clamp(left, -1, 1);
60
0
        right = clamp(right, -1, 1);
61
0
    }
62
63
    // Logarithmic scaling, as audio should ALWAYS do.
64
    // Reference: https://www.dr-lex.be/info-stuff/volumecontrols.html
65
    // We use the curve `factor = a * exp(b * change)`,
66
    // where change is the input fraction we want to change by,
67
    // a = 1/1000, b = ln(1000) = 6.908 and factor is the multiplier used.
68
    // The value 1000 represents the dynamic range in sound pressure, which corresponds to 60 dB(A).
69
    // This is a good dynamic range because it can represent all loudness values from
70
    // 30 dB(A) (barely hearable with background noise)
71
    // to 90 dB(A) (almost too loud to hear and about the reasonable limit of actual sound equipment).
72
    //
73
    // Format ranges:
74
    // - Linear:        0.0 to 1.0
75
    // - Logarithmic:   0.0 to 1.0
76
77
    ALWAYS_INLINE float linear_to_log(float const change) const
78
0
    {
79
0
        // TODO: Add linear slope around 0
80
0
        return VOLUME_A * exp(VOLUME_B * change);
81
0
    }
82
83
    ALWAYS_INLINE float log_to_linear(float const val) const
84
0
    {
85
0
        // TODO: Add linear slope around 0
86
0
        return log(val / VOLUME_A) / VOLUME_B;
87
0
    }
88
89
    ALWAYS_INLINE Sample& log_multiply(float const change)
90
0
    {
91
0
        float factor = linear_to_log(change);
92
0
        left *= factor;
93
0
        right *= factor;
94
0
        return *this;
95
0
    }
96
97
    ALWAYS_INLINE Sample log_multiplied(float const volume_change) const
98
0
    {
99
0
        Sample new_frame { left, right };
100
0
        new_frame.log_multiply(volume_change);
101
0
        return new_frame;
102
0
    }
103
104
    // Constant power panning
105
    ALWAYS_INLINE Sample& pan(float const position)
106
0
    {
107
0
        float const pi_over_2 = AK::Pi<float> * 0.5f;
108
0
        float const root_over_2 = AK::sqrt<float>(2.0) * 0.5f;
109
0
        float const angle = position * pi_over_2 * 0.5f;
110
0
        float s, c;
111
0
        AK::sincos<float>(angle, s, c);
112
0
        left *= root_over_2 * (c - s);
113
0
        right *= root_over_2 * (c + s);
114
0
        return *this;
115
0
    }
116
117
    ALWAYS_INLINE Sample panned(float const position) const
118
0
    {
119
0
        Sample new_sample { left, right };
120
0
        new_sample.pan(position);
121
0
        return new_sample;
122
0
    }
123
124
    constexpr Sample& operator*=(float const mult)
125
0
    {
126
0
        left *= mult;
127
0
        right *= mult;
128
0
        return *this;
129
0
    }
130
131
    constexpr Sample operator*(float const mult) const
132
0
    {
133
0
        return { left * mult, right * mult };
134
0
    }
135
136
    constexpr Sample& operator+=(Sample const& other)
137
0
    {
138
0
        left += other.left;
139
0
        right += other.right;
140
0
        return *this;
141
0
    }
142
    constexpr Sample& operator+=(float other)
143
0
    {
144
0
        left += other;
145
0
        right += other;
146
0
        return *this;
147
0
    }
148
149
    constexpr Sample operator+(Sample const& other) const
150
0
    {
151
0
        return { left + other.left, right + other.right };
152
0
    }
153
154
    float left { 0 };
155
    float right { 0 };
156
};
157
158
}
159
160
namespace AK {
161
162
template<>
163
struct Formatter<Audio::Sample> : Formatter<FormatString> {
164
    ErrorOr<void> format(FormatBuilder& builder, Audio::Sample const& value)
165
0
    {
166
0
        return Formatter<FormatString>::format(builder, "[{}, {}]"sv, value.left, value.right);
167
0
    }
168
};
169
170
}