summaryrefslogtreecommitdiff
path: root/Kernel/Devices/Audio
diff options
context:
space:
mode:
authorJelle Raaijmakers <jelle@gmta.nl>2023-03-10 20:10:06 +0100
committerJelle Raaijmakers <jelle@gmta.nl>2023-03-25 21:27:03 +0100
commitdd8fa73da179d6a805ecdae62ff87e8992d43821 (patch)
treed7a6a563dc2a18fa368f9adcb4605ce695b05c05 /Kernel/Devices/Audio
parentc530f74e2f0210da393bc1904659b521275097b0 (diff)
downloadserenity-dd8fa73da179d6a805ecdae62ff87e8992d43821.zip
Kernel: Add support for Intel HDA
This is an implementation that tries to follow the spec as closely as possible, and works with Qemu's Intel HDA and some bare metal HDA controllers out there. Compiling with `INTEL_HDA_DEBUG=on` will provide a lot of detailed information that could help us getting this to work on more bare metal controllers as well :^) Output format is limited to `i16` samples for now.
Diffstat (limited to 'Kernel/Devices/Audio')
-rw-r--r--Kernel/Devices/Audio/IntelHDA/Codec.cpp721
-rw-r--r--Kernel/Devices/Audio/IntelHDA/Codec.h497
-rw-r--r--Kernel/Devices/Audio/IntelHDA/Controller.cpp342
-rw-r--r--Kernel/Devices/Audio/IntelHDA/Controller.h85
-rw-r--r--Kernel/Devices/Audio/IntelHDA/Format.cpp140
-rw-r--r--Kernel/Devices/Audio/IntelHDA/Format.h23
-rw-r--r--Kernel/Devices/Audio/IntelHDA/OutputPath.h149
-rw-r--r--Kernel/Devices/Audio/IntelHDA/RingBuffer.h250
-rw-r--r--Kernel/Devices/Audio/IntelHDA/Stream.cpp249
-rw-r--r--Kernel/Devices/Audio/IntelHDA/Stream.h103
-rw-r--r--Kernel/Devices/Audio/IntelHDA/Timing.h30
-rw-r--r--Kernel/Devices/Audio/Management.cpp46
12 files changed, 2616 insertions, 19 deletions
diff --git a/Kernel/Devices/Audio/IntelHDA/Codec.cpp b/Kernel/Devices/Audio/IntelHDA/Codec.cpp
new file mode 100644
index 0000000000..4d6abe3c21
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/Codec.cpp
@@ -0,0 +1,721 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "Codec.h"
+#include <AK/Array.h>
+#include <Kernel/Devices/Audio/IntelHDA/Controller.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+// 7.3.4.7: Supported PCM Size, Rates
+struct BitRateEncoding {
+ u8 flag;
+ u8 bit_rate;
+};
+static constexpr Array<BitRateEncoding, 5> bit_rate_encodings { {
+ // clang-format off
+ { 0x1, 8 },
+ { 0x2, 16 },
+ { 0x4, 20 },
+ { 0x8, 24 },
+ { 0x10, 32 },
+ // clang-format on
+} };
+
+struct SampleRateEncoding {
+ u16 flag;
+ u32 sample_rate;
+};
+static constexpr Array<SampleRateEncoding, 12> sample_rate_encodings { {
+ // clang-format off
+ { 0x1, 8'000 },
+ { 0x2, 11'025 },
+ { 0x4, 16'000 },
+ { 0x8, 22'050 },
+ { 0x10, 32'000 },
+ { 0x20, 44'100 },
+ { 0x40, 48'000 },
+ { 0x80, 88'200 },
+ { 0x100, 96'000 },
+ { 0x200, 176'400 },
+ { 0x400, 192'000 },
+ { 0x800, 384'000 },
+ // clang-format on
+} };
+
+ErrorOr<void> Codec::register_node(NonnullRefPtr<Node> node)
+{
+ auto set_result = TRY(m_nodes_by_node_id.try_set(node->node_id(), node));
+ VERIFY(set_result == HashSetResult::InsertedNewEntry);
+ return {};
+}
+
+ErrorOr<void> Node::initialize()
+{
+ return codec().register_node(*this);
+}
+
+ErrorOr<u32> Node::command(CodecControlVerb verb, u16 payload)
+{
+ auto& node_codec = codec();
+ return node_codec.controller().send_command(node_codec.codec_address(), m_node_id, verb, payload);
+}
+
+ErrorOr<u32> Node::parameter(GetParameterId get_parameter_id)
+{
+ return command(CodecControlVerb::GetParameter, to_underlying(get_parameter_id));
+}
+
+ErrorOr<void> Node::set_power_state(PowerState power_state)
+{
+ // 7.3.3.10: Power State
+ TRY(command(CodecControlVerb::SetPowerState, to_underlying(power_state)));
+ return {};
+}
+
+template<typename T>
+void NodeWithChildren<T>::for_each_child_node(Function<void(T const&, bool)> callback) const
+{
+ auto number_of_child_nodes = m_child_nodes.size();
+ for (size_t child_index = 0; child_index < number_of_child_nodes; ++child_index)
+ callback(m_child_nodes[child_index], child_index == number_of_child_nodes - 1);
+}
+
+template<typename T>
+ErrorOr<void> NodeWithChildren<T>::initialize()
+{
+ TRY(Node::initialize());
+ return populate_child_nodes();
+}
+
+template<typename T>
+ErrorOr<void> NodeWithChildren<T>::populate_child_nodes()
+{
+ VERIFY(m_child_nodes.is_empty());
+
+ // 7.3.4.3: Subordinate Node Count
+ auto subordinate_node_count = TRY(parameter(GetParameterId::SubordinateNodeCount));
+ u8 starting_node_number = (subordinate_node_count >> 16) & 0xff;
+ u8 total_number_of_nodes = subordinate_node_count & 0xff;
+ TRY(m_child_nodes.try_ensure_capacity(total_number_of_nodes));
+ for (int subnode_index = 0; subnode_index < total_number_of_nodes; ++subnode_index)
+ m_child_nodes.unchecked_append(TRY(Node::create<T>(*this, starting_node_number + subnode_index)));
+ return {};
+}
+
+StringView WidgetNode::widget_type_name() const
+{
+ switch (m_widget_type) {
+ case WidgetType::AudioInput:
+ return "Audio Input"sv;
+ case WidgetType::AudioMixer:
+ return "Audio Mixer"sv;
+ case WidgetType::AudioOutput:
+ return "Audio Output"sv;
+ case WidgetType::AudioSelector:
+ return "Audio Selector"sv;
+ case WidgetType::BeepGenerator:
+ return "Beep Generator"sv;
+ case WidgetType::PinComplex:
+ return "Pin Complex"sv;
+ case WidgetType::Power:
+ return "Power"sv;
+ case WidgetType::VendorDefined:
+ return "Vendor Defined"sv;
+ case WidgetType::VolumeKnob:
+ return "Volume Knob"sv;
+ }
+ return "Reserved"sv;
+}
+
+ErrorOr<void> WidgetNode::initialize()
+{
+ TRY(Node::initialize());
+
+ // 7.3.4.6: Audio Widget Capabilities
+ auto widget_capabilities = TRY(parameter(GetParameterId::AudioWidgetCapabilities));
+ m_widget_type = static_cast<WidgetType>((widget_capabilities >> 20) & 0xf);
+ m_channel_count = (((widget_capabilities >> 15) & 0xe) | (widget_capabilities & 0x1)) + 1;
+ m_power_control_supported = (widget_capabilities & WidgetCapabilityFlag::PowerControlSupported) > 0;
+ m_connection_list_present = (widget_capabilities & WidgetCapabilityFlag::ConnectionListPresent) > 0;
+ m_format_override = (widget_capabilities & WidgetCapabilityFlag::FormatOverride) > 0;
+ m_amp_param_override = (widget_capabilities & WidgetCapabilityFlag::AmpParamOverride) > 0;
+ m_output_amp_present = (widget_capabilities & WidgetCapabilityFlag::OutputAmpPresent) > 0;
+ m_input_amp_present = (widget_capabilities & WidgetCapabilityFlag::InputAmpPresent) > 0;
+
+ if (supports_stream()) {
+ // 7.3.3.11: Converter Stream, Channel
+ auto stream_channel = TRY(command(CodecControlVerb::GetConverterStreamChannel, 0));
+ m_selected_stream = (stream_channel >> 4) & 0xf;
+ m_selected_channel = stream_channel & 0xf;
+
+ TRY(populate_supported_pcm_size_rates());
+ TRY(populate_supported_stream_formats());
+ }
+
+ // 7.3.4.10: Amplifier Capabilities
+ auto read_amp_capabilities = [](Node& node, GetParameterId type) -> ErrorOr<AmplifierCapabilities> {
+ auto capabilities = TRY(node.parameter(type));
+ return AmplifierCapabilities {
+ .muting_supported = ((capabilities >> 31) & 0x1) > 0,
+ .step_size = static_cast<u8>((capabilities >> 16) & 0x7f),
+ .number_of_steps = static_cast<u8>(((capabilities >> 8) & 0x7f) + 1),
+ .offset = static_cast<u8>(capabilities & 0x7f),
+ };
+ };
+ Node& amp_params_node = amp_param_override() ? *this : *parent_node();
+ if (output_amp_present())
+ m_output_amp_capabilities = TRY(read_amp_capabilities(amp_params_node, GetParameterId::OutputAmplifierCapabilities));
+ if (input_amp_present())
+ m_input_amp_capabilities = TRY(read_amp_capabilities(amp_params_node, GetParameterId::InputAmplifierCapabilities));
+
+ if (widget_type() == WidgetType::PinComplex) {
+ // 7.3.4.9: Pin Capabilities
+ auto pin_capabilities = TRY(parameter(GetParameterId::PinCapabilities));
+ m_pin_complex_input_supported = (pin_capabilities & PinCapabilityFlag::InputCapable) > 0;
+ m_pin_complex_output_supported = (pin_capabilities & PinCapabilityFlag::OutputCapable) > 0;
+
+ TRY(populate_pin_configuration_default());
+ }
+
+ // Connection list
+ if (connection_list_present())
+ TRY(populate_connection_list());
+
+ return {};
+}
+
+ErrorOr<NonnullOwnPtr<KString>> WidgetNode::to_string()
+{
+ StringBuilder builder;
+ TRY(builder.try_appendff("WidgetNode(node_id={}, type={})", node_id(), widget_type_name()));
+ return KString::try_create(builder.string_view());
+}
+
+void WidgetNode::debug_dump(StringView group_spine, bool is_last) const
+{
+ dbgln("{} {} Widget (node #{}):", group_spine, is_last ? "โ””"sv : "โ”œ"sv, node_id());
+ auto spine = is_last ? " "sv : "โ”‚"sv;
+ dbgln("{} {} โ”œ Type: {} ({:#x})", group_spine, spine, widget_type_name(), to_underlying(widget_type()));
+ dbgln("{} {} โ”œ Channel count: {}", group_spine, spine, channel_count());
+ dbgln("{} {} โ”œ Power control supported: {}", group_spine, spine, m_power_control_supported ? "yes"sv : "no"sv);
+
+ if (supports_stream()) {
+ dbgln("{} {} โ”œ Selected stream: {}", group_spine, spine, selected_stream());
+ if (channel_count() == 1)
+ dbgln("{} {} โ”œ Selected channel: {}", group_spine, spine, selected_channel());
+ else
+ dbgln("{} {} โ”œ Selected channels: {}-{}", group_spine, spine, selected_channel(), selected_channel() + channel_count() - 1);
+
+ dbgln("{} {} โ”œ Format override: {}", group_spine, spine, format_override() ? "yes"sv : "no"sv);
+ dbgln("{} {} โ”œ Supported PCM bit sizes:", group_spine, spine);
+ for (auto supported_size : supported_pcm_sizes())
+ dbgln("{} {} โ”‚ โ€ข {}", group_spine, spine, supported_size);
+
+ dbgln("{} {} โ”œ Supported PCM rates:", group_spine, spine);
+ for (auto supported_rate : supported_pcm_rates())
+ dbgln("{} {} โ”‚ โ€ข {}Hz", group_spine, spine, supported_rate);
+
+ dbgln("{} {} โ”œ Supported stream formats:", group_spine, spine);
+ if (has_flag(supported_stream_formats(), StreamFormatFlag::PCM))
+ dbgln("{} {} โ”‚ โ€ข PCM", group_spine, spine);
+ if (has_flag(supported_stream_formats(), StreamFormatFlag::Float32))
+ dbgln("{} {} โ”‚ โ€ข Float32", group_spine, spine);
+ if (has_flag(supported_stream_formats(), StreamFormatFlag::AC3))
+ dbgln("{} {} โ”‚ โ€ข AC3", group_spine, spine);
+ }
+
+ dbgln("{} {} โ”œ Amplifier parameters override: {}", group_spine, spine, amp_param_override() ? "yes"sv : "no"sv);
+ dbgln("{} {} โ”œ Output amplifier present: {}", group_spine, spine, output_amp_present() ? "yes"sv : "no"sv);
+ if (output_amp_present()) {
+ auto amp_capabilities = output_amp_capabilities();
+ dbgln("{} {} โ”‚ โ”œ Muting supported: {}", group_spine, spine, amp_capabilities.muting_supported ? "yes"sv : "no"sv);
+ dbgln("{} {} โ”‚ โ”œ Step size: {}", group_spine, spine, amp_capabilities.step_size);
+ dbgln("{} {} โ”‚ โ”œ Number of steps: {}", group_spine, spine, amp_capabilities.number_of_steps);
+ dbgln("{} {} โ”‚ โ”” Offset: {}", group_spine, spine, amp_capabilities.offset);
+ }
+
+ dbgln("{} {} โ”œ Input amplifier present: {}", group_spine, spine, input_amp_present() ? "yes"sv : "no"sv);
+ if (input_amp_present()) {
+ auto amp_capabilities = input_amp_capabilities();
+ dbgln("{} {} โ”‚ โ”œ Muting supported: {}", group_spine, spine, amp_capabilities.muting_supported ? "yes"sv : "no"sv);
+ dbgln("{} {} โ”‚ โ”œ Step size: {}", group_spine, spine, amp_capabilities.step_size);
+ dbgln("{} {} โ”‚ โ”œ Number of steps: {}", group_spine, spine, amp_capabilities.number_of_steps);
+ dbgln("{} {} โ”‚ โ”” Offset: {}", group_spine, spine, amp_capabilities.offset);
+ }
+
+ if (widget_type() == WidgetType::PinComplex) {
+ dbgln("{} {} โ”œ Pin complex input supported: {}", group_spine, spine, pin_complex_input_supported());
+ dbgln("{} {} โ”œ Pin complex output supported: {}", group_spine, spine, pin_complex_output_supported());
+ dbgln("{} {} โ”œ Pin configuration default:", group_spine, spine);
+ dbgln("{} {} โ”‚ โ”œ Sequence: {}", group_spine, spine, m_pin_configuration_default.sequence);
+ dbgln("{} {} โ”‚ โ”œ Default association: {}", group_spine, spine, m_pin_configuration_default.default_association);
+ dbgln("{} {} โ”‚ โ”œ Jack detect override: {}", group_spine, spine,
+ ((static_cast<u8>(m_pin_configuration_default.misc) & to_underlying(PinMiscFlag::JackDetectOverride)) > 0) ? "yes"sv : "no"sv);
+ dbgln("{} {} โ”‚ โ”œ Color: {}", group_spine, spine, pin_color_name());
+ dbgln("{} {} โ”‚ โ”œ Connection type: {}", group_spine, spine, pin_connection_type_name());
+ dbgln("{} {} โ”‚ โ”œ Default device: {}", group_spine, spine, pin_default_device_name());
+ dbgln("{} {} โ”‚ โ”œ Location: {}, {}", group_spine, spine, pin_gross_location_name(), pin_geometric_location_name());
+ dbgln("{} {} โ”‚ โ”” Port connectivity: {}", group_spine, spine, pin_port_connectivity_name());
+ }
+
+ dbgln("{} {} โ”” Connection list:{}", group_spine, spine, connection_list_present() ? ""sv : " absent"sv);
+ if (connection_list_present()) {
+ auto selected_node_id = connection_selected_node_id();
+ auto all_active = !supports_connection_select_control();
+ for (auto connection_entry : connection_list()) {
+ dbgln("{} {} โ€ข Node #{}{}", group_spine, spine, connection_entry,
+ all_active || connection_entry == selected_node_id ? " (active)"sv : ""sv);
+ }
+ }
+}
+
+ErrorOr<void> WidgetNode::set_amplifier_gain_mute(SetAmplifierGainMute settings)
+{
+ // 7.3.3.7: Amplifier Gain/Mute
+ VERIFY(input_amp_present() || output_amp_present());
+ u16 set_amp_gain_payload = ((output_amp_present() ? 1 : 0) << 15)
+ | ((input_amp_present() ? 1 : 0) << 15)
+ | ((settings.set_left ? 1 : 0) << 13)
+ | ((settings.set_right ? 1 : 0) << 12)
+ | ((settings.connection_index & 0xf) << 8)
+ | ((settings.mute ? 1 : 0) << 7)
+ | (settings.gain & 0x7f);
+ TRY(command(CodecControlVerb::SetAmplifierGainMute, set_amp_gain_payload));
+ return {};
+}
+
+ErrorOr<void> WidgetNode::set_connection_select(u8 connection_index)
+{
+ // 7.3.3.2: Connection Select Control
+ VERIFY(connection_list_present());
+ VERIFY(connection_index < connection_list().size());
+ TRY(command(CodecControlVerb::SetConnectionSelectControl, connection_index));
+ return {};
+}
+
+ErrorOr<void> WidgetNode::set_converter_stream_and_channel(u8 stream_index, u8 channel_index)
+{
+ // 7.3.3.11: Converter Stream, Channel
+ VERIFY(widget_type() == WidgetType::AudioInput || widget_type() == WidgetType::AudioOutput);
+ u16 stream_channel_payload = ((stream_index & 0xf) << 4) | (channel_index & 0xf);
+ TRY(command(CodecControlVerb::SetConverterStreamChannel, stream_channel_payload));
+ return {};
+}
+
+ErrorOr<void> WidgetNode::set_pin_control(PinControl pin_control)
+{
+ // 7.3.3.13: Pin Widget Control
+ VERIFY(widget_type() == WidgetType::PinComplex);
+ VERIFY(!pin_control.output_enabled || pin_complex_output_supported());
+ VERIFY(!pin_control.input_enabled || pin_complex_input_supported());
+
+ u8 payload = ((pin_control.low_impedance_amplifier_enabled ? 1 : 0) << 7)
+ | ((pin_control.output_enabled ? 1 : 0) << 6)
+ | ((pin_control.input_enabled ? 1 : 0) << 5)
+ | (pin_control.voltage_reference_enable & 0x7);
+ TRY(command(CodecControlVerb::SetPinWidgetControl, payload));
+ return {};
+}
+
+bool WidgetNode::supports_stream() const
+{
+ return widget_type() == WidgetType::AudioInput
+ || widget_type() == WidgetType::AudioOutput;
+}
+
+bool WidgetNode::supports_connection_select_control() const
+{
+ return widget_type() == WidgetType::AudioInput
+ || widget_type() == WidgetType::AudioSelector
+ || widget_type() == WidgetType::PinComplex;
+}
+
+ErrorOr<FormatParameters> WidgetNode::get_converter_format()
+{
+ // 7.3.3.8: Converter Format
+ VERIFY(widget_type() == WidgetType::AudioInput || widget_type() == WidgetType::AudioOutput);
+ u16 format = TRY(command(CodecControlVerb::GetConverterFormat, 0)) & 0xffffu;
+ return decode_format(format);
+}
+
+ErrorOr<void> WidgetNode::set_converter_format(FormatParameters format)
+{
+ // 7.3.3.8: Converter Format
+ VERIFY(widget_type() == WidgetType::AudioInput || widget_type() == WidgetType::AudioOutput);
+ u16 format_payload = TRY(encode_format(format));
+ TRY(command(CodecControlVerb::SetConverterFormat, format_payload));
+ return {};
+}
+
+ErrorOr<void> WidgetNode::populate_supported_pcm_size_rates()
+{
+ VERIFY(m_supported_pcm_sizes.is_empty() && m_supported_pcm_rates.is_empty());
+
+ // 7.3.4.7: Supported PCM Size, Rates
+ Node& stream_support_node = format_override() ? *this : *parent_node();
+ auto supported_pcm_size_and_rates = TRY(stream_support_node.parameter(GetParameterId::SupportedPCMSizeRates));
+
+ auto pcm_sizes = (supported_pcm_size_and_rates >> 16) & 0x1f;
+ TRY(m_supported_pcm_sizes.try_ensure_capacity(popcount(pcm_sizes)));
+ for (auto bit_rate_encoding : bit_rate_encodings) {
+ if ((pcm_sizes & bit_rate_encoding.flag) > 0)
+ m_supported_pcm_sizes.unchecked_append(bit_rate_encoding.bit_rate);
+ }
+
+ auto pcm_rates = supported_pcm_size_and_rates & 0x7ff;
+ TRY(m_supported_pcm_rates.try_ensure_capacity(popcount(pcm_rates)));
+ for (auto sample_rate_encoding : sample_rate_encodings) {
+ if ((pcm_rates & sample_rate_encoding.flag) > 0)
+ m_supported_pcm_rates.unchecked_append(sample_rate_encoding.sample_rate);
+ }
+
+ return {};
+}
+
+ErrorOr<void> WidgetNode::populate_supported_stream_formats()
+{
+ VERIFY(m_supported_stream_formats == 0);
+
+ // 7.3.4.8: Supported Stream Formats
+ Node& stream_support_node = format_override() ? *this : *parent_node();
+ auto supported_stream_formats = TRY(stream_support_node.parameter(GetParameterId::SupportedStreamFormats));
+
+ if ((supported_stream_formats & 0x1) > 0)
+ m_supported_stream_formats |= StreamFormatFlag::PCM;
+ if ((supported_stream_formats & 0x2) > 0)
+ m_supported_stream_formats |= StreamFormatFlag::Float32;
+ if ((supported_stream_formats & 0x4) > 0)
+ m_supported_stream_formats |= StreamFormatFlag::AC3;
+
+ return {};
+}
+
+ErrorOr<void> WidgetNode::populate_connection_list()
+{
+ VERIFY(connection_list_present());
+ VERIFY(m_connection_list.is_empty());
+
+ // 7.3.4.11: Connection List Length
+ auto connection_list_length_info = TRY(parameter(GetParameterId::ConnectionListLength));
+ bool long_form = (connection_list_length_info >> 7) & 0x1;
+ u8 connection_list_length = connection_list_length_info & 0x7f;
+ u8 entries_per_request = long_form ? 2 : 4;
+
+ // 7.3.3.3: Get Connection List Entry
+ for (u8 entry_offset = 0; entry_offset < connection_list_length; entry_offset += entries_per_request) {
+ auto entries = TRY(command(CodecControlVerb::GetConnectionListEntry, entry_offset));
+ for (u8 entry_index = 0; entry_index < min(entries_per_request, static_cast<int>(connection_list_length) - entry_offset); ++entry_index) {
+ u16 entry = entries & (long_form ? 0xffff : 0xff);
+ TRY(m_connection_list.try_append(entry));
+ entries >>= (32 / entries_per_request);
+ }
+ }
+
+ // 7.1.3: Widget Interconnection Rules
+ // "Connection_List_Length = 1 means there is only one (hard-wired) input possible and,
+ // therefore, there is no Connection_Selector field. The actual connection is read
+ // from the Connection List as usual."
+ if (connection_list_length == 1) {
+ m_connection_index = 0;
+ } else {
+ // 7.3.3.2: Connection Select Control
+ auto connection_selection_control = TRY(command(CodecControlVerb::GetConnectionSelectControl, 0));
+ m_connection_index = connection_selection_control & 0xff;
+ }
+
+ return {};
+}
+
+ErrorOr<void> WidgetNode::populate_pin_configuration_default()
+{
+ VERIFY(widget_type() == WidgetType::PinComplex);
+
+ u32 configuration_default = TRY(command(CodecControlVerb::GetConfigurationDefault, 0));
+ m_pin_configuration_default.sequence = configuration_default & 0xf;
+ m_pin_configuration_default.default_association = (configuration_default >> 4) & 0xf;
+ m_pin_configuration_default.misc = static_cast<PinMiscFlag>((configuration_default >> 8) & 0xf);
+ m_pin_configuration_default.color = static_cast<PinColor>((configuration_default >> 12) & 0xf);
+ m_pin_configuration_default.connection_type = static_cast<PinConnectionType>((configuration_default >> 16) & 0xf);
+ m_pin_configuration_default.default_device = static_cast<PinDefaultDevice>((configuration_default >> 20) & 0xf);
+ m_pin_configuration_default.geometric_location = static_cast<PinGeometricLocation>((configuration_default >> 24) & 0xf);
+ m_pin_configuration_default.gross_location = static_cast<PinGrossLocation>((configuration_default >> 28) & 0x3);
+ m_pin_configuration_default.port_connectivity = static_cast<PinPortConnectivity>(configuration_default >> 30);
+
+ return {};
+}
+
+StringView WidgetNode::pin_color_name() const
+{
+ auto underlying_color = to_underlying(m_pin_configuration_default.color);
+ if (underlying_color >= 0xa && underlying_color <= 0xd)
+ return "Reserved"sv;
+
+ switch (m_pin_configuration_default.color) {
+ case PinColor::Unknown:
+ return "Unknown"sv;
+ case PinColor::Black:
+ return "Black"sv;
+ case PinColor::Grey:
+ return "Grey"sv;
+ case PinColor::Blue:
+ return "Blue"sv;
+ case PinColor::Green:
+ return "Green"sv;
+ case PinColor::Red:
+ return "Red"sv;
+ case PinColor::Orange:
+ return "Orange"sv;
+ case PinColor::Yellow:
+ return "Yellow"sv;
+ case PinColor::Purple:
+ return "Purple"sv;
+ case PinColor::Pink:
+ return "Pink"sv;
+ case PinColor::White:
+ return "White"sv;
+ case PinColor::Other:
+ return "Other"sv;
+ }
+ VERIFY_NOT_REACHED();
+}
+
+StringView WidgetNode::pin_connection_type_name() const
+{
+ switch (m_pin_configuration_default.connection_type) {
+ case PinConnectionType::Unknown:
+ return "Unknown"sv;
+ case PinConnectionType::EighthStereoMono:
+ return "1/8\" Stereo/Mono"sv;
+ case PinConnectionType::FourthStereoMono:
+ return "1/4\" Stereo/Mono"sv;
+ case PinConnectionType::ATAPIInternal:
+ return "ATAPI Internal"sv;
+ case PinConnectionType::RCA:
+ return "RCA"sv;
+ case PinConnectionType::Optical:
+ return "Optical"sv;
+ case PinConnectionType::OtherDigital:
+ return "Other Digital"sv;
+ case PinConnectionType::OtherAnalog:
+ return "Other Analog"sv;
+ case PinConnectionType::MultichannelAnalog:
+ return "Multichannel Analog"sv;
+ case PinConnectionType::XLRProfessional:
+ return "XLR / Professional"sv;
+ case PinConnectionType::RJ11:
+ return "RJ-11 (Modem)"sv;
+ case PinConnectionType::Combination:
+ return "Combination"sv;
+ case PinConnectionType::Other:
+ return "Other"sv;
+ }
+ VERIFY_NOT_REACHED();
+}
+
+StringView WidgetNode::pin_default_device_name() const
+{
+ switch (m_pin_configuration_default.default_device) {
+ case PinDefaultDevice::LineOut:
+ return "Line Out"sv;
+ case PinDefaultDevice::Speaker:
+ return "Speaker"sv;
+ case PinDefaultDevice::HPOut:
+ return "Headphones"sv;
+ case PinDefaultDevice::CD:
+ return "CD"sv;
+ case PinDefaultDevice::SPDIFOut:
+ return "S/PDIF Out"sv;
+ case PinDefaultDevice::DigitalOtherOut:
+ return "Digital Other Out"sv;
+ case PinDefaultDevice::ModemLineSide:
+ return "Modem Line Side"sv;
+ case PinDefaultDevice::ModemHandsetSide:
+ return "Modem Handset Side"sv;
+ case PinDefaultDevice::LineIn:
+ return "Line In"sv;
+ case PinDefaultDevice::AUX:
+ return "AUX"sv;
+ case PinDefaultDevice::MicIn:
+ return "Mic In"sv;
+ case PinDefaultDevice::Telephony:
+ return "Telephony"sv;
+ case PinDefaultDevice::SPDIFIn:
+ return "S/PDIF In"sv;
+ case PinDefaultDevice::DigitalOtherIn:
+ return "Digital Other In"sv;
+ case PinDefaultDevice::Reserved:
+ return "Reserved"sv;
+ case PinDefaultDevice::Other:
+ return "Other"sv;
+ }
+ VERIFY_NOT_REACHED();
+}
+
+StringView WidgetNode::pin_gross_location_name() const
+{
+ switch (m_pin_configuration_default.gross_location) {
+ case PinGrossLocation::ExternalOnPrimaryChassis:
+ return "External on Primary Chassis"sv;
+ case PinGrossLocation::Internal:
+ return "Internal"sv;
+ case PinGrossLocation::SeparateChassis:
+ return "Separate Chassis"sv;
+ case PinGrossLocation::Other:
+ return "Other"sv;
+ }
+ VERIFY_NOT_REACHED();
+}
+
+StringView WidgetNode::pin_geometric_location_name() const
+{
+ // 7.3.3.31: Configuration Default - special cases
+ if (m_pin_configuration_default.geometric_location == PinGeometricLocation::Special1) {
+ if (m_pin_configuration_default.gross_location == PinGrossLocation::ExternalOnPrimaryChassis)
+ return "Rear Panel"sv;
+ if (m_pin_configuration_default.gross_location == PinGrossLocation::Internal)
+ return "Riser"sv;
+ if (m_pin_configuration_default.gross_location == PinGrossLocation::Other)
+ return "Mobile Lid (Inside)"sv;
+ } else if (m_pin_configuration_default.geometric_location == PinGeometricLocation::Special2) {
+ if (m_pin_configuration_default.gross_location == PinGrossLocation::ExternalOnPrimaryChassis)
+ return "Drive Bay"sv;
+ if (m_pin_configuration_default.gross_location == PinGrossLocation::Internal)
+ return "Digital Display"sv;
+ if (m_pin_configuration_default.gross_location == PinGrossLocation::Other)
+ return "Mobile Lid (Outside)"sv;
+ } else if (m_pin_configuration_default.geometric_location == PinGeometricLocation::Special3) {
+ if (m_pin_configuration_default.gross_location == PinGrossLocation::Internal)
+ return "ATAPI"sv;
+ }
+
+ switch (m_pin_configuration_default.geometric_location) {
+ case PinGeometricLocation::NotApplicable:
+ return "N/A"sv;
+ case PinGeometricLocation::Rear:
+ return "Rear"sv;
+ case PinGeometricLocation::Front:
+ return "Front"sv;
+ case PinGeometricLocation::Left:
+ return "Left"sv;
+ case PinGeometricLocation::Right:
+ return "Right"sv;
+ case PinGeometricLocation::Top:
+ return "Top"sv;
+ case PinGeometricLocation::Bottom:
+ return "Bottom"sv;
+ case PinGeometricLocation::Special1:
+ case PinGeometricLocation::Special2:
+ case PinGeometricLocation::Special3:
+ return "Special"sv;
+ }
+ VERIFY_NOT_REACHED();
+}
+
+StringView WidgetNode::pin_port_connectivity_name() const
+{
+ switch (m_pin_configuration_default.port_connectivity) {
+ case PinPortConnectivity::Jack:
+ return "Jack"sv;
+ case PinPortConnectivity::NoConnection:
+ return "No Physical Connection"sv;
+ case PinPortConnectivity::FixedFunction:
+ return "Fixed Function Device"sv;
+ case PinPortConnectivity::JackAndFixedFunction:
+ return "Jack and Fixed Function Device"sv;
+ }
+ VERIFY_NOT_REACHED();
+}
+
+StringView FunctionGroupNode::function_group_type_name() const
+{
+ switch (m_function_group_type) {
+ case FunctionGroupType::AudioFunctionGroup:
+ return "Audio Function Group"sv;
+ case FunctionGroupType::ModemFunctionGroup:
+ return "Modem Function Group"sv;
+ case FunctionGroupType::VendorFunctionGroup:
+ return "Vendor Function Group"sv;
+ case FunctionGroupType::Reserved:
+ return "Reserved"sv;
+ }
+ VERIFY_NOT_REACHED();
+}
+
+ErrorOr<void> FunctionGroupNode::initialize()
+{
+ TRY(NodeWithChildren<WidgetNode>::initialize());
+
+ // 7.3.4.4: Function Group Type
+ auto function_group_type = TRY(parameter(GetParameterId::FunctionGroupType));
+ if (function_group_type == 0x1)
+ m_function_group_type = FunctionGroupType::AudioFunctionGroup;
+ else if (function_group_type == 0x2)
+ m_function_group_type = FunctionGroupType::ModemFunctionGroup;
+ else if (function_group_type >= 0x80)
+ m_function_group_type = FunctionGroupType::VendorFunctionGroup;
+ else
+ m_function_group_type = FunctionGroupType::Reserved;
+
+ return {};
+}
+
+ErrorOr<NonnullOwnPtr<KString>> FunctionGroupNode::to_string()
+{
+ StringBuilder builder;
+ TRY(builder.try_appendff("FunctionGroupNode(node_id={})", node_id()));
+ return KString::try_create(builder.string_view());
+}
+
+void FunctionGroupNode::debug_dump(bool is_last) const
+{
+ dbgln("{} Function group (node #{}):", is_last ? "โ””"sv : "โ”œ"sv, node_id());
+ auto spine = is_last ? " "sv : "โ”‚"sv;
+ dbgln("{} โ”œ Function group type: {} ({:#x})", spine, function_group_type_name(), to_underlying(function_group_type()));
+
+ for_each_child_node([&spine](WidgetNode const& widget_node, bool is_last) -> void {
+ widget_node.debug_dump(spine, is_last);
+ });
+}
+
+ErrorOr<void> RootNode::initialize()
+{
+ TRY(NodeWithChildren<FunctionGroupNode>::initialize());
+
+ // 7.3.4.1: Vendor ID
+ auto vendor_id_response = TRY(parameter(GetParameterId::VendorID));
+ m_vendor_id = (vendor_id_response >> 16) & 0xffff;
+ m_device_id = vendor_id_response & 0xffff;
+
+ // 7.3.4.2: Revision ID
+ auto revision_id_response = TRY(parameter(GetParameterId::RevisionID));
+ m_major_revision = (revision_id_response >> 20) & 0xf;
+ m_minor_revision = (revision_id_response >> 16) & 0xf;
+ if (m_major_revision != 1 || m_minor_revision != 0)
+ return ENOTSUP;
+
+ return {};
+}
+
+ErrorOr<NonnullOwnPtr<KString>> RootNode::to_string()
+{
+ StringBuilder builder;
+ TRY(builder.try_appendff("RootNode(node_id={})", node_id()));
+ return KString::try_create(builder.string_view());
+}
+
+void RootNode::debug_dump() const
+{
+ dbgln("Root (node #{}):", node_id());
+ dbgln("โ”œ Codec vendor: {:#04x}, device: {:#04x}", vendor_id(), device_id());
+ dbgln("โ”œ Codec HDA compatibility: {}.{}", major_revision(), minor_revision());
+
+ for_each_child_node([](FunctionGroupNode const& fg_node, bool is_last) -> void {
+ fg_node.debug_dump(is_last);
+ });
+}
+
+}
diff --git a/Kernel/Devices/Audio/IntelHDA/Codec.h b/Kernel/Devices/Audio/IntelHDA/Codec.h
new file mode 100644
index 0000000000..f1dd94d0da
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/Codec.h
@@ -0,0 +1,497 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Error.h>
+#include <AK/HashMap.h>
+#include <AK/NonnullOwnPtr.h>
+#include <AK/NonnullRefPtr.h>
+#include <AK/RefCounted.h>
+#include <AK/RefPtr.h>
+#include <AK/Vector.h>
+#include <Kernel/Devices/Audio/IntelHDA/Format.h>
+#include <Kernel/KString.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+class Codec;
+class Controller;
+class RootNode;
+
+// 7.3.3: Controls
+enum CodecControlVerb : u16 {
+ GetParameter = 0xf00,
+ GetConnectionSelectControl = 0xf01,
+ SetConnectionSelectControl = 0x701,
+ GetConnectionListEntry = 0xf02,
+ GetAmplifierGainMute = 0xb,
+ SetAmplifierGainMute = 0x3,
+ GetConverterFormat = 0xa,
+ SetConverterFormat = 0x2,
+ SetPowerState = 0x705,
+ GetConverterStreamChannel = 0xf06,
+ SetConverterStreamChannel = 0x706,
+ SetPinWidgetControl = 0x707,
+ GetConfigurationDefault = 0xf1c,
+};
+
+// 7.3.4.8: Supported Stream Formats, figure 88
+enum StreamFormatFlag : u8 {
+ PCM = 1u << 0,
+ Float32 = 1u << 1,
+ AC3 = 1u << 2,
+};
+AK_ENUM_BITWISE_OPERATORS(StreamFormatFlag);
+
+class Node : public RefCounted<Node> {
+public:
+ enum class NodeType {
+ Root,
+ FunctionGroup,
+ Widget,
+ };
+
+ // 7.3.3.10: Power State, table 83
+ enum class PowerState : u8 {
+ D0 = 0b000,
+ D1 = 0b001,
+ D2 = 0b010,
+ D3 = 0b011,
+ D3Cold = 0b100,
+ };
+
+ // 7.3.4: Parameters
+ enum class GetParameterId : u8 {
+ VendorID = 0x00,
+ RevisionID = 0x02,
+ SubordinateNodeCount = 0x04,
+ FunctionGroupType = 0x05,
+ AudioFunctionGroupCapabilities = 0x08,
+ AudioWidgetCapabilities = 0x09,
+ SupportedPCMSizeRates = 0x0a,
+ SupportedStreamFormats = 0x0b,
+ PinCapabilities = 0x0c,
+ InputAmplifierCapabilities = 0x0d,
+ ConnectionListLength = 0x0e,
+ SupportedPowerStates = 0x0f,
+ ProcessingCapabilities = 0x10,
+ GPIOCount = 0x11,
+ OutputAmplifierCapabilities = 0x12,
+ VolumeKnobCapabilities = 0x13,
+ };
+
+ virtual ~Node() = default;
+
+ template<typename T, class... Args>
+ static ErrorOr<NonnullRefPtr<T>> create(Args&&... args)
+ requires(IsBaseOf<Node, T>)
+ {
+ auto node = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) T(forward<Args>(args)...)));
+ TRY(node->initialize());
+ return node;
+ }
+
+ virtual Codec& codec()
+ {
+ VERIFY(m_parent_node);
+ return m_parent_node->codec();
+ }
+
+ NodeType node_type() const { return m_node_type; }
+ RefPtr<Node> parent_node() const { return m_parent_node; }
+ u8 node_id() const { return m_node_id; }
+
+ ErrorOr<u32> command(CodecControlVerb, u16 payload);
+ ErrorOr<u32> parameter(GetParameterId);
+ ErrorOr<void> set_power_state(PowerState);
+ virtual ErrorOr<NonnullOwnPtr<KString>> to_string() = 0;
+
+protected:
+ Node(NodeType node_type, RefPtr<Node> parent_node, u8 node_id)
+ : m_node_type(node_type)
+ , m_parent_node(parent_node)
+ , m_node_id(node_id)
+ {
+ }
+
+ virtual ErrorOr<void> initialize();
+
+ NodeType m_node_type;
+ RefPtr<Node> m_parent_node;
+ u8 m_node_id;
+};
+
+template<typename T>
+class NodeWithChildren : public Node {
+ friend class Node;
+
+public:
+ Vector<NonnullRefPtr<T>> child_nodes() { return m_child_nodes; }
+ void for_each_child_node(Function<void(T const&, bool)> callback) const;
+
+protected:
+ NodeWithChildren(NodeType node_type, RefPtr<Node> parent_node, u8 node_id)
+ : Node(node_type, parent_node, node_id)
+ {
+ }
+
+ ErrorOr<void> initialize() override;
+
+private:
+ ErrorOr<void> populate_child_nodes();
+
+ Vector<NonnullRefPtr<T>> m_child_nodes {};
+};
+
+class WidgetNode final : public Node {
+ friend class Node;
+
+public:
+ static constexpr NodeType Type = NodeType::Widget;
+
+ // 7.3.4.6: Audio Widget Capabilities, figure 86
+ enum WidgetCapabilityFlag : u32 {
+ InputAmpPresent = 1u << 1,
+ OutputAmpPresent = 1u << 2,
+ AmpParamOverride = 1u << 3,
+ FormatOverride = 1u << 4,
+ ConnectionListPresent = 1u << 8,
+ PowerControlSupported = 1u << 10,
+ };
+
+ // 7.3.4.6: Audio Widget Capabilities, table 138
+ enum WidgetType : u8 {
+ AudioOutput = 0x0,
+ AudioInput = 0x1,
+ AudioMixer = 0x2,
+ AudioSelector = 0x3,
+ PinComplex = 0x4,
+ Power = 0x5,
+ VolumeKnob = 0x6,
+ BeepGenerator = 0x7,
+ VendorDefined = 0xf,
+ };
+
+ // 7.3.4.9: Pin Capabilities, figure 89
+ enum PinCapabilityFlag : u32 {
+ OutputCapable = 1u << 4,
+ InputCapable = 1u << 5,
+ };
+
+ // 7.3.4.10: Amplifier Capabilities
+ struct AmplifierCapabilities {
+ bool muting_supported;
+ u8 step_size;
+ u8 number_of_steps;
+ u8 offset;
+ };
+
+ // 7.3.3.7: Amplifier Gain/Mute Set Payload
+ struct SetAmplifierGainMute {
+ bool set_left { true };
+ bool set_right { true };
+ u8 connection_index { 0 };
+ bool mute;
+ u8 gain;
+ };
+
+ // 7.3.3.13: Pin Widget Control
+ struct PinControl {
+ bool low_impedance_amplifier_enabled { true };
+ bool output_enabled { false };
+ bool input_enabled { false };
+ u8 voltage_reference_enable { 0 };
+ };
+
+ // 7.3.3.31: Configuration Default, table 109
+ enum class PinPortConnectivity : u8 {
+ Jack = 0b00,
+ NoConnection = 0b01,
+ FixedFunction = 0b10,
+ JackAndFixedFunction = 0b11,
+ };
+
+ // 7.3.3.31: Configuration Default, table 110 (rows)
+ enum class PinGrossLocation : u8 {
+ ExternalOnPrimaryChassis = 0b00,
+ Internal = 0b01,
+ SeparateChassis = 0b10,
+ Other = 0b11,
+ };
+
+ // 7.3.3.31: Configuration Default, table 110 (columns)
+ enum class PinGeometricLocation : u8 {
+ NotApplicable = 0x0,
+ Rear = 0x1,
+ Front = 0x2,
+ Left = 0x3,
+ Right = 0x4,
+ Top = 0x5,
+ Bottom = 0x6,
+ Special1 = 0x7,
+ Special2 = 0x8,
+ Special3 = 0x9,
+ };
+
+ // 7.3.3.31: Configuration Default, table 111
+ enum class PinDefaultDevice : u8 {
+ LineOut = 0x0,
+ Speaker = 0x1,
+ HPOut = 0x2,
+ CD = 0x3,
+ SPDIFOut = 0x4,
+ DigitalOtherOut = 0x5,
+ ModemLineSide = 0x6,
+ ModemHandsetSide = 0x7,
+ LineIn = 0x8,
+ AUX = 0x9,
+ MicIn = 0xa,
+ Telephony = 0xb,
+ SPDIFIn = 0xc,
+ DigitalOtherIn = 0xd,
+ Reserved = 0xe,
+ Other = 0xf,
+ };
+
+ // 7.3.3.31: Configuration Default, table 112
+ enum class PinConnectionType : u8 {
+ Unknown = 0x0,
+ EighthStereoMono = 0x1,
+ FourthStereoMono = 0x2,
+ ATAPIInternal = 0x3,
+ RCA = 0x4,
+ Optical = 0x5,
+ OtherDigital = 0x6,
+ OtherAnalog = 0x7,
+ MultichannelAnalog = 0x8,
+ XLRProfessional = 0x9,
+ RJ11 = 0xa,
+ Combination = 0xb,
+ Other = 0xf,
+ };
+
+ // 7.3.3.31: Configuration Default, table 113
+ enum class PinColor : u8 {
+ Unknown = 0x0,
+ Black = 0x1,
+ Grey = 0x2,
+ Blue = 0x3,
+ Green = 0x4,
+ Red = 0x5,
+ Orange = 0x6,
+ Yellow = 0x7,
+ Purple = 0x8,
+ Pink = 0x9,
+ White = 0xe,
+ Other = 0xf,
+ };
+
+ // 7.3.3.31: Configuration Default, table 114
+ enum class PinMiscFlag : u8 {
+ JackDetectOverride = 1u << 0,
+ };
+
+ // 7.3.3.31: Configuration Default, figure 74
+ struct PinConfigurationDefault {
+ PinPortConnectivity port_connectivity;
+ PinGrossLocation gross_location;
+ PinGeometricLocation geometric_location;
+ PinDefaultDevice default_device;
+ PinConnectionType connection_type;
+ PinColor color;
+ PinMiscFlag misc;
+ u8 default_association;
+ u8 sequence;
+ };
+
+ WidgetType widget_type() const { return m_widget_type; }
+ StringView widget_type_name() const;
+
+ u8 channel_count() const { return m_channel_count; }
+ bool power_control_supported() const { return m_power_control_supported; }
+ bool connection_list_present() const { return m_connection_list_present; }
+ bool format_override() const { return m_format_override; }
+ bool amp_param_override() const { return m_amp_param_override; }
+ bool output_amp_present() const { return m_output_amp_present; }
+ bool input_amp_present() const { return m_input_amp_present; }
+ u8 selected_stream() const { return m_selected_stream; }
+ u8 selected_channel() const { return m_selected_channel; }
+ Span<u8 const> supported_pcm_sizes() const { return m_supported_pcm_sizes.span(); }
+ Span<u32 const> supported_pcm_rates() const { return m_supported_pcm_rates.span(); }
+ StreamFormatFlag supported_stream_formats() const { return m_supported_stream_formats; }
+ AmplifierCapabilities output_amp_capabilities() const { return m_output_amp_capabilities; }
+ AmplifierCapabilities input_amp_capabilities() const { return m_input_amp_capabilities; }
+ bool pin_complex_input_supported() const { return m_pin_complex_input_supported; }
+ bool pin_complex_output_supported() const { return m_pin_complex_output_supported; }
+ Span<u8 const> connection_list() const { return m_connection_list.span(); }
+ u8 connection_selected_node_id() const { return m_connection_list[m_connection_index]; }
+
+ PinConfigurationDefault pin_configuration_default() const { return m_pin_configuration_default; }
+ StringView pin_color_name() const;
+ StringView pin_connection_type_name() const;
+ StringView pin_default_device_name() const;
+ StringView pin_gross_location_name() const;
+ StringView pin_geometric_location_name() const;
+ StringView pin_port_connectivity_name() const;
+
+ ErrorOr<NonnullOwnPtr<KString>> to_string() override;
+ void debug_dump(StringView, bool) const;
+ ErrorOr<void> set_amplifier_gain_mute(SetAmplifierGainMute);
+ ErrorOr<void> set_connection_select(u8);
+ ErrorOr<void> set_converter_stream_and_channel(u8 stream_index, u8 channel_index);
+ ErrorOr<void> set_pin_control(PinControl);
+ bool supports_stream() const;
+ bool supports_connection_select_control() const;
+
+ ErrorOr<FormatParameters> get_converter_format();
+ ErrorOr<void> set_converter_format(FormatParameters);
+
+protected:
+ ErrorOr<void> initialize() override;
+
+private:
+ WidgetNode(NonnullRefPtr<Node> parent_node, u8 node_id)
+ : Node(NodeType::Widget, parent_node, node_id)
+ {
+ }
+
+ ErrorOr<void> populate_supported_pcm_size_rates();
+ ErrorOr<void> populate_supported_stream_formats();
+ ErrorOr<void> populate_connection_list();
+ ErrorOr<void> populate_pin_configuration_default();
+
+ WidgetType m_widget_type;
+ u8 m_channel_count;
+ bool m_power_control_supported;
+ bool m_connection_list_present;
+ bool m_format_override;
+ bool m_amp_param_override;
+ bool m_output_amp_present;
+ bool m_input_amp_present;
+ u8 m_selected_stream;
+ u8 m_selected_channel;
+ Vector<u8> m_supported_pcm_sizes {};
+ Vector<u32> m_supported_pcm_rates {};
+ StreamFormatFlag m_supported_stream_formats { 0 };
+ AmplifierCapabilities m_output_amp_capabilities;
+ AmplifierCapabilities m_input_amp_capabilities;
+ bool m_pin_complex_input_supported;
+ bool m_pin_complex_output_supported;
+ PinConfigurationDefault m_pin_configuration_default;
+ Vector<u8> m_connection_list {};
+ u8 m_connection_index;
+};
+
+class FunctionGroupNode final : public NodeWithChildren<WidgetNode> {
+ friend class Node;
+
+public:
+ static constexpr NodeType Type = NodeType::FunctionGroup;
+
+ // 7.3.4.4: Function Group Type
+ enum class FunctionGroupType {
+ AudioFunctionGroup,
+ ModemFunctionGroup,
+ VendorFunctionGroup,
+ Reserved,
+ };
+
+ ErrorOr<NonnullOwnPtr<KString>> to_string() override;
+ void debug_dump(bool) const;
+
+ FunctionGroupType function_group_type() const { return m_function_group_type; }
+ StringView function_group_type_name() const;
+
+protected:
+ ErrorOr<void> initialize() override;
+
+private:
+ FunctionGroupNode(NonnullRefPtr<Node> parent_node, u8 node_id)
+ : NodeWithChildren<WidgetNode>(NodeType::FunctionGroup, parent_node, node_id)
+ {
+ }
+
+ FunctionGroupType m_function_group_type;
+};
+
+class RootNode final : public NodeWithChildren<FunctionGroupNode> {
+ friend class Node;
+
+public:
+ static constexpr NodeType Type = NodeType::Root;
+
+ Codec& codec() override { return m_codec; }
+
+ u16 vendor_id() const { return m_vendor_id; }
+ u16 device_id() const { return m_device_id; }
+ u8 major_revision() const { return m_major_revision; }
+ u8 minor_revision() const { return m_minor_revision; }
+
+ ErrorOr<NonnullOwnPtr<KString>> to_string() override;
+ void debug_dump() const;
+
+protected:
+ ErrorOr<void> initialize() override;
+
+private:
+ RootNode(Codec& codec)
+ : NodeWithChildren<FunctionGroupNode>(NodeType::Root, {}, 0)
+ , m_codec(codec)
+ {
+ }
+
+ Codec& m_codec;
+ u16 m_vendor_id;
+ u16 m_device_id;
+ u8 m_major_revision;
+ u8 m_minor_revision;
+};
+
+class Codec : public RefCounted<Codec> {
+public:
+ static ErrorOr<NonnullRefPtr<Codec>> create(Controller& controller, u8 codec_address)
+ {
+ return adopt_nonnull_ref_or_enomem(new (nothrow) Codec(controller, codec_address));
+ }
+
+ Controller& controller() const { return m_controller; }
+ u8 codec_address() const { return m_codec_address; }
+
+ RefPtr<RootNode> root_node() const { return m_root_node; }
+ void set_root_node(NonnullRefPtr<RootNode> root_node) { m_root_node = root_node; }
+
+ ErrorOr<void> register_node(NonnullRefPtr<Node> node);
+ Optional<Node*> node_by_node_id(u8 node_id) { return m_nodes_by_node_id.get(node_id); }
+
+ template<typename T, typename TPredicate>
+ ErrorOr<Vector<NonnullRefPtr<T>>> nodes_matching(TPredicate predicate)
+ {
+ Vector<NonnullRefPtr<T>> results;
+ for (auto node_entry : m_nodes_by_node_id) {
+ if (node_entry.value->node_type() != T::Type)
+ continue;
+ auto node = NonnullRefPtr<T> { *reinterpret_cast<WidgetNode*>(node_entry.value.ptr()) };
+ if (predicate(node))
+ TRY(results.try_append(node));
+ }
+ return results;
+ }
+
+private:
+ Codec(Controller& controller, u8 codec_address)
+ : m_controller(controller)
+ , m_codec_address(codec_address)
+ {
+ }
+
+ Controller& m_controller;
+ u8 m_codec_address;
+ RefPtr<RootNode> m_root_node;
+ HashMap<u8, NonnullRefPtr<Node>> m_nodes_by_node_id;
+};
+
+}
diff --git a/Kernel/Devices/Audio/IntelHDA/Controller.cpp b/Kernel/Devices/Audio/IntelHDA/Controller.cpp
new file mode 100644
index 0000000000..60787e0e79
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/Controller.cpp
@@ -0,0 +1,342 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "Controller.h"
+#include <AK/Optional.h>
+#include <AK/Vector.h>
+#include <Kernel/Arch/Delay.h>
+#include <Kernel/Bus/PCI/API.h>
+#include <Kernel/Devices/Audio/IntelHDA/Codec.h>
+#include <Kernel/Devices/Audio/IntelHDA/Stream.h>
+#include <Kernel/Devices/Audio/IntelHDA/Timing.h>
+#include <Kernel/Time/TimeManagement.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+UNMAP_AFTER_INIT ErrorOr<NonnullLockRefPtr<Controller>> Controller::create(PCI::DeviceIdentifier const& pci_device_identifier)
+{
+ auto controller_io_window = TRY(IOWindow::create_for_pci_device_bar(pci_device_identifier, PCI::HeaderType0BaseRegister::BAR0));
+
+ auto intel_hda = TRY(adopt_nonnull_lock_ref_or_enomem(new (nothrow) Controller(pci_device_identifier, move(controller_io_window))));
+ TRY(intel_hda->initialize());
+ return intel_hda;
+}
+
+UNMAP_AFTER_INIT Controller::Controller(PCI::DeviceIdentifier const& pci_device_identifier, NonnullOwnPtr<IOWindow> controller_io_window)
+ : PCI::Device(const_cast<PCI::DeviceIdentifier&>(pci_device_identifier))
+ , m_controller_io_window(move(controller_io_window))
+{
+}
+
+UNMAP_AFTER_INIT ErrorOr<void> Controller::initialize()
+{
+ // Enable DMA
+ PCI::enable_bus_mastering(device_identifier());
+
+ // 3.3.3, 3.3.4: Controller version
+ auto version_minor = m_controller_io_window->read8(ControllerRegister::VersionMinor);
+ auto version_major = m_controller_io_window->read8(ControllerRegister::VersionMajor);
+ dmesgln_pci(*this, "Intel High Definition Audio specification v{}.{}", version_major, version_minor);
+ if (version_major != 1 || version_minor != 0)
+ return ENOTSUP;
+
+ // 3.3.2: Read capabilities
+ u16 capabilities = m_controller_io_window->read16(ControllerRegister::GlobalCapabilities);
+ dbgln_if(INTEL_HDA_DEBUG, "Controller capabilities:");
+ m_number_of_output_streams = capabilities >> 12;
+ m_number_of_input_streams = (capabilities >> 8) & 0xf;
+ m_number_of_bidirectional_streams = (capabilities >> 3) & 0x1f;
+ bool is_64_bit_addressing_supported = (capabilities & 0x1) > 0;
+ dbgln_if(INTEL_HDA_DEBUG, "โ”œ Number of output streams: {}", m_number_of_output_streams);
+ dbgln_if(INTEL_HDA_DEBUG, "โ”œ Number of input streams: {}", m_number_of_input_streams);
+ dbgln_if(INTEL_HDA_DEBUG, "โ”œ Number of bidirectional streams: {}", m_number_of_bidirectional_streams);
+ dbgln_if(INTEL_HDA_DEBUG, "โ”” 64-bit addressing supported: {}", is_64_bit_addressing_supported ? "yes" : "no");
+ if (m_number_of_output_streams == 0)
+ return ENOTSUP;
+ if (!is_64_bit_addressing_supported && sizeof(FlatPtr) == 8)
+ return ENOTSUP;
+
+ // Reset the controller
+ TRY(reset());
+
+ // Register CORB and RIRB
+ auto command_io_window = TRY(m_controller_io_window->create_from_io_window_with_offset(ControllerRegister::CommandOutboundRingBufferOffset));
+ m_command_buffer = TRY(CommandOutboundRingBuffer::create("IntelHDA CORB"sv, move(command_io_window)));
+ TRY(m_command_buffer->register_with_controller());
+
+ auto response_io_window = TRY(m_controller_io_window->create_from_io_window_with_offset(ControllerRegister::ResponseInboundRingBufferOffset));
+ m_response_buffer = TRY(ResponseInboundRingBuffer::create("IntelHDA RIRB"sv, move(response_io_window)));
+ TRY(m_response_buffer->register_with_controller());
+
+ dbgln_if(INTEL_HDA_DEBUG, "CORB ({} entries) and RIRB ({} entries) registered", m_command_buffer->capacity(), m_response_buffer->capacity());
+
+ // Initialize all codecs
+ // 3.3.9: State Change Status
+ u16 state_change_status = m_controller_io_window->read16(ControllerRegister::StateChangeStatus);
+ for (u8 codec_address = 0; codec_address < 14; ++codec_address) {
+ if ((state_change_status & (1 << codec_address)) > 0) {
+ dmesgln_pci(*this, "Found codec on address #{}", codec_address);
+ TRY(initialize_codec(codec_address));
+ }
+ }
+
+ return {};
+}
+
+UNMAP_AFTER_INIT ErrorOr<void> Controller::initialize_codec(u8 codec_address)
+{
+ auto codec = TRY(Codec::create(*this, codec_address));
+
+ auto root_node = TRY(Node::create<RootNode>(codec));
+ if constexpr (INTEL_HDA_DEBUG)
+ root_node->debug_dump();
+ codec->set_root_node(root_node);
+
+ TRY(m_codecs.try_append(codec));
+
+ return {};
+}
+
+ErrorOr<u32> Controller::send_command(u8 codec_address, u8 node_id, CodecControlVerb verb, u16 payload)
+{
+ // Construct command
+ // 7.3: If the most significant 4 bits of 12-bits verb are 0xf or 0x7, extended mode is selected
+ u32 command_value = codec_address << 28 | (node_id << 20);
+ if (((verb & 0x700) > 0) || ((verb & 0xf00) > 0))
+ command_value |= ((verb & 0xfff) << 8) | (payload & 0xff);
+ else
+ command_value |= ((verb & 0xf) << 16) | payload;
+
+ dbgln_if(INTEL_HDA_DEBUG, "Controller::{}: codec {} node {} verb {:#x} payload {:#b}",
+ __FUNCTION__, codec_address, node_id, to_underlying(verb), payload);
+ TRY(m_command_buffer->write_value(command_value));
+
+ // Read response
+ Optional<u64> full_response;
+ TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() -> ErrorOr<bool> {
+ full_response = TRY(m_response_buffer->read_value());
+ return full_response.has_value();
+ }));
+ u32 response = full_response.value() & 0xffffffffu;
+ dbgln_if(INTEL_HDA_DEBUG, "Controller::{}: response {:#032b}", __FUNCTION__, response);
+ return response;
+}
+
+UNMAP_AFTER_INIT ErrorOr<void> Controller::configure_output_route()
+{
+ Vector<NonnullRefPtr<WidgetNode>> queued_nodes;
+ Vector<WidgetNode*> visited_nodes;
+ HashMap<WidgetNode*, WidgetNode*> parents;
+
+ auto create_output_path = [&](RefPtr<WidgetNode> found_node) -> ErrorOr<NonnullOwnPtr<OutputPath>> {
+ // Reconstruct path by traversing parent nodes
+ Vector<NonnullRefPtr<WidgetNode>> path;
+ auto path_node = found_node;
+ while (path_node) {
+ TRY(path.try_append(*path_node));
+ path_node = parents.get(path_node).value_or(nullptr);
+ }
+ path.reverse();
+
+ // Create output stream
+ constexpr u8 output_stream_index = 0;
+ constexpr u8 output_stream_number = 1;
+ u64 output_stream_offset = ControllerRegister::StreamsOffset
+ + m_number_of_input_streams * 0x20
+ + output_stream_index * 0x20;
+ auto stream_io_window = TRY(m_controller_io_window->create_from_io_window_with_offset(output_stream_offset));
+ auto output_stream = TRY(OutputStream::create(move(stream_io_window), output_stream_number));
+
+ // Create output path
+ auto output_path = TRY(OutputPath::create(move(path), move(output_stream)));
+ TRY(output_path->activate());
+ return output_path;
+ };
+
+ for (auto codec : m_codecs) {
+ // Start off by finding all candidate pin complexes
+ auto pin_widgets = TRY(codec->nodes_matching<WidgetNode>([](NonnullRefPtr<WidgetNode> node) {
+ // Find pin complexes that support output.
+ if (node->widget_type() != WidgetNode::WidgetType::PinComplex
+ || !node->pin_complex_output_supported())
+ return false;
+
+ // Only consider pin complexes that have:
+ // - a physical connection (jack or fixed function)
+ // - and a default device that is line out, speakers or headphones.
+ auto configuration_default = node->pin_configuration_default();
+ auto port_connectivity = configuration_default.port_connectivity;
+ auto default_device = configuration_default.default_device;
+
+ bool is_physically_connected = port_connectivity == WidgetNode::PinPortConnectivity::Jack
+ || port_connectivity == WidgetNode::PinPortConnectivity::FixedFunction
+ || port_connectivity == WidgetNode::PinPortConnectivity::JackAndFixedFunction;
+ bool is_output_device = default_device == WidgetNode::PinDefaultDevice::LineOut
+ || default_device == WidgetNode::PinDefaultDevice::Speaker
+ || default_device == WidgetNode::PinDefaultDevice::HPOut;
+
+ return is_physically_connected && is_output_device;
+ }));
+
+ // Perform a breadth-first search to find a path to an audio output widget
+ for (auto pin_widget : pin_widgets) {
+ VERIFY(queued_nodes.is_empty() && visited_nodes.is_empty() && parents.is_empty());
+
+ TRY(queued_nodes.try_append(pin_widget));
+ Optional<NonnullRefPtr<WidgetNode>> found_node = {};
+ while (!queued_nodes.is_empty()) {
+ auto current_node = queued_nodes.take_first();
+ if (current_node->widget_type() == WidgetNode::AudioOutput) {
+ found_node = current_node;
+ break;
+ }
+
+ TRY(visited_nodes.try_append(current_node.ptr()));
+ for (u8 connection_node_id : current_node->connection_list()) {
+ auto connection_node = codec->node_by_node_id(connection_node_id);
+ if (!connection_node.has_value() || connection_node.value()->node_type() != Node::NodeType::Widget) {
+ dmesgln_pci(*this, "Warning: connection node {} does not exist or is the wrong type", connection_node_id);
+ continue;
+ }
+
+ auto connection_widget = NonnullRefPtr<WidgetNode> { *reinterpret_cast<WidgetNode*>(connection_node.release_value()) };
+ if (visited_nodes.contains_slow(connection_widget))
+ continue;
+
+ TRY(queued_nodes.try_append(connection_widget));
+ TRY(parents.try_set(connection_widget, current_node.ptr()));
+ }
+ }
+
+ if (found_node.has_value()) {
+ m_output_path = TRY(create_output_path(found_node.release_value()));
+ break;
+ }
+
+ queued_nodes.clear_with_capacity();
+ visited_nodes.clear_with_capacity();
+ parents.clear_with_capacity();
+ }
+
+ if (m_output_path)
+ break;
+ }
+
+ if (!m_output_path) {
+ dmesgln_pci(*this, "Failed to find an audio output path");
+ return ENODEV;
+ }
+
+ // We are ready to go!
+ dmesgln_pci(*this, "Successfully configured an audio output path");
+ dbgln_if(INTEL_HDA_DEBUG, "{}", TRY(m_output_path->to_string()));
+
+ return {};
+}
+
+ErrorOr<void> Controller::reset()
+{
+ // 3.3.7: "Controller Reset (CRST): Writing a 0 to this bit causes the High Definition Audio
+ // controller to transition to the Reset state."
+ u32 global_control = m_controller_io_window->read32(ControllerRegister::GlobalControl);
+ global_control &= ~GlobalControlFlag::ControllerReset;
+ global_control &= ~GlobalControlFlag::AcceptUnsolicitedResponseEnable;
+ m_controller_io_window->write32(ControllerRegister::GlobalControl, global_control);
+
+ // 3.3.7: "After the hardware has completed sequencing into the reset state, it will report
+ // a 0 in this bit. Software must read a 0 from this bit to verify that the
+ // controller is in reset."
+ TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() {
+ global_control = m_controller_io_window->read32(ControllerRegister::GlobalControl);
+ return (global_control & GlobalControlFlag::ControllerReset) == 0;
+ }));
+
+ // 3.3.7: "Writing a 1 to this bit causes the controller to exit its Reset state and
+ // de-assert the link RESET# signal. Software is responsible for
+ // setting/clearing this bit such that the minimum link RESET# signal assertion
+ // pulse width specification is met (see Section 5.5)."
+ microseconds_delay(100);
+ global_control |= GlobalControlFlag::ControllerReset;
+ m_controller_io_window->write32(ControllerRegister::GlobalControl, global_control);
+
+ // 3.3.7: "When the controller hardware is ready to begin operation, it will report a 1 in
+ // this bit. Software must read a 1 from this bit before accessing any controller
+ // registers."
+ TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() {
+ global_control = m_controller_io_window->read32(ControllerRegister::GlobalControl);
+ return (global_control & GlobalControlFlag::ControllerReset) > 0;
+ }));
+
+ // 4.3 Codec Discovery:
+ // "The software must wait at least 521 us (25 frames) after reading CRST as a 1 before
+ // assuming that codecs have all made status change requests and have been registered
+ // by the controller."
+ microseconds_delay(frame_delay_in_microseconds(25));
+
+ dbgln_if(INTEL_HDA_DEBUG, "Controller reset");
+ return {};
+}
+
+LockRefPtr<AudioChannel> Controller::audio_channel(u32 index) const
+{
+ if (index != fixed_audio_channel_index)
+ return {};
+ return m_audio_channel;
+}
+
+ErrorOr<size_t> Controller::write(size_t channel_index, UserOrKernelBuffer const& data, size_t length)
+{
+ if (channel_index != fixed_audio_channel_index || !m_output_path)
+ return ENODEV;
+ return m_output_path->output_stream().write(data, length);
+}
+
+UNMAP_AFTER_INIT void Controller::detect_hardware_audio_channels(Badge<AudioManagement>)
+{
+ auto result = configure_output_route();
+ if (result.is_error()) {
+ dmesgln_pci(*this, "Failed to set up an output audio channel: {}", result.error());
+ return;
+ }
+
+ m_audio_channel = AudioChannel::must_create(*this, fixed_audio_channel_index);
+}
+
+ErrorOr<void> Controller::set_pcm_output_sample_rate(size_t channel_index, u32 samples_per_second_rate)
+{
+ if (channel_index != fixed_audio_channel_index || !m_output_path)
+ return ENODEV;
+
+ TRY(m_output_path->set_format({
+ .sample_rate = samples_per_second_rate,
+ .pcm_bits = OutputPath::fixed_pcm_bits,
+ .number_of_channels = OutputPath::fixed_channel_count,
+ }));
+ dmesgln_pci(*this, "Set output channel #{} PCM rate: {} Hz", channel_index, samples_per_second_rate);
+ return {};
+}
+
+ErrorOr<u32> Controller::get_pcm_output_sample_rate(size_t channel_index)
+{
+ if (channel_index != fixed_audio_channel_index || !m_output_path)
+ return ENODEV;
+
+ return m_output_path->output_stream().sample_rate();
+}
+
+ErrorOr<void> wait_until(size_t delay_in_microseconds, size_t timeout_in_microseconds, Function<ErrorOr<bool>()> condition)
+{
+ auto const& time_management = TimeManagement::the();
+ u64 start_microseconds = time_management.now().to_microseconds();
+ while (!TRY(condition())) {
+ microseconds_delay(delay_in_microseconds);
+ if ((time_management.now().to_microseconds() - start_microseconds) >= timeout_in_microseconds)
+ return ETIMEDOUT;
+ }
+ return {};
+}
+
+}
diff --git a/Kernel/Devices/Audio/IntelHDA/Controller.h b/Kernel/Devices/Audio/IntelHDA/Controller.h
new file mode 100644
index 0000000000..6abd20705b
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/Controller.h
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Error.h>
+#include <AK/NonnullOwnPtr.h>
+#include <AK/NonnullRefPtr.h>
+#include <AK/Vector.h>
+#include <Kernel/Bus/PCI/Device.h>
+#include <Kernel/Devices/Audio/Channel.h>
+#include <Kernel/Devices/Audio/Controller.h>
+#include <Kernel/Devices/Audio/IntelHDA/OutputPath.h>
+#include <Kernel/Devices/Audio/IntelHDA/RingBuffer.h>
+#include <Kernel/IOWindow.h>
+#include <Kernel/Library/LockRefPtr.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+// Specification: https://www.intel.com/content/dam/www/public/us/en/documents/product-specifications/high-definition-audio-specification.pdf
+
+class Codec;
+
+class Controller final
+ : public AudioController
+ , public PCI::Device {
+public:
+ static ErrorOr<NonnullLockRefPtr<Controller>> create(PCI::DeviceIdentifier const&);
+ virtual ~Controller() = default;
+
+ // ^PCI::Device
+ virtual StringView device_name() const override { return "IntelHDA"sv; }
+
+ ErrorOr<u32> send_command(u8 codec_address, u8 node_id, CodecControlVerb verb, u16 payload);
+
+private:
+ static constexpr size_t fixed_audio_channel_index = 0;
+
+ // 3.3: High Definition Audio Controller Register Set
+ enum ControllerRegister : u8 {
+ GlobalCapabilities = 0x00,
+ VersionMinor = 0x02,
+ VersionMajor = 0x03,
+ GlobalControl = 0x08,
+ StateChangeStatus = 0x0e,
+ CommandOutboundRingBufferOffset = 0x40,
+ ResponseInboundRingBufferOffset = 0x50,
+ StreamsOffset = 0x80,
+ };
+
+ // 3.3.7: GCTL โ€“ Global Control
+ enum GlobalControlFlag : u32 {
+ ControllerReset = 1u << 0,
+ AcceptUnsolicitedResponseEnable = 1u << 8,
+ };
+
+ Controller(PCI::DeviceIdentifier const&, NonnullOwnPtr<IOWindow>);
+
+ ErrorOr<void> initialize();
+ ErrorOr<void> initialize_codec(u8 codec_address);
+ ErrorOr<void> configure_output_route();
+ ErrorOr<void> reset();
+
+ // ^AudioController
+ virtual LockRefPtr<AudioChannel> audio_channel(u32 index) const override;
+ virtual ErrorOr<size_t> write(size_t channel_index, UserOrKernelBuffer const& data, size_t length) override;
+ virtual void detect_hardware_audio_channels(Badge<AudioManagement>) override;
+ virtual ErrorOr<void> set_pcm_output_sample_rate(size_t channel_index, u32 samples_per_second_rate) override;
+ virtual ErrorOr<u32> get_pcm_output_sample_rate(size_t channel_index) override;
+
+ NonnullOwnPtr<IOWindow> m_controller_io_window;
+ u8 m_number_of_output_streams;
+ u8 m_number_of_input_streams;
+ u8 m_number_of_bidirectional_streams;
+ OwnPtr<CommandOutboundRingBuffer> m_command_buffer;
+ OwnPtr<ResponseInboundRingBuffer> m_response_buffer;
+ Vector<NonnullRefPtr<Codec>> m_codecs {};
+ OwnPtr<OutputPath> m_output_path;
+ LockRefPtr<AudioChannel> m_audio_channel;
+};
+
+}
diff --git a/Kernel/Devices/Audio/IntelHDA/Format.cpp b/Kernel/Devices/Audio/IntelHDA/Format.cpp
new file mode 100644
index 0000000000..64f26e0379
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/Format.cpp
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "Format.h"
+#include <AK/Array.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+// 3.3.41: Input/Output/Bidirectional Stream Descriptor Format
+// 3.7.1: Stream Format Structure
+struct SampleRateParameters {
+ u32 sample_rate;
+ u8 base;
+ u8 multiple;
+ u8 divisor;
+};
+static constexpr Array<SampleRateParameters, 15> sample_rate_parameters { {
+ // clang-format off
+ { 6'000, 0b0, 0b000, 0b111 },
+ { 8'000, 0b0, 0b000, 0b101 },
+ { 9'600, 0b0, 0b000, 0b100 },
+ { 11'025, 0b1, 0b000, 0b011 },
+ { 16'000, 0b0, 0b000, 0b010 },
+ { 22'050, 0b1, 0b000, 0b001 },
+ { 24'000, 0b0, 0b000, 0b001 },
+ { 32'000, 0b0, 0b001, 0b010 },
+ { 44'100, 0b1, 0b000, 0b000 },
+ { 48'000, 0b0, 0b000, 0b000 },
+ { 88'200, 0b1, 0b001, 0b000 },
+ { 96'000, 0b0, 0b001, 0b000 },
+ { 144'000, 0b0, 0b010, 0b000 },
+ { 176'400, 0b1, 0b011, 0b000 },
+ { 192'000, 0b0, 0b011, 0b000 },
+ // clang-format on
+} };
+
+struct PcmBitsParameters {
+ u8 pcm_bits;
+ u8 encoding;
+};
+static constexpr Array<PcmBitsParameters, 5> pcm_bits_parameters { {
+ // clang-format off
+ { 8, 0b000 },
+ { 16, 0b001 },
+ { 20, 0b010 },
+ { 24, 0b011 },
+ { 32, 0b100 },
+ // clang-format on
+} };
+
+ErrorOr<u16> encode_format(FormatParameters format)
+{
+ // 3.3.41: Input/Output/Bidirectional Stream Descriptor Format
+ // 3.7.1: Stream Format Structure
+
+ // Stream type
+ // NOTE: we only support PCM streams
+ auto is_pcm = true;
+
+ // Sample rate parameters
+ Optional<SampleRateParameters> selected_sample_rate {};
+ for (auto sample_rate_parameter : sample_rate_parameters) {
+ if (sample_rate_parameter.sample_rate == format.sample_rate) {
+ selected_sample_rate = sample_rate_parameter;
+ break;
+ }
+ }
+ if (!selected_sample_rate.has_value())
+ return ENOTSUP;
+
+ // Bit size
+ Optional<PcmBitsParameters> selected_bit_rate {};
+ for (auto pcm_bits_parameter : pcm_bits_parameters) {
+ if (pcm_bits_parameter.pcm_bits == format.pcm_bits) {
+ selected_bit_rate = pcm_bits_parameter;
+ break;
+ }
+ }
+ if (!selected_bit_rate.has_value())
+ return ENOTSUP;
+
+ // Number of channels
+ if (format.number_of_channels < 1 || format.number_of_channels > 16)
+ return ENOTSUP;
+
+ // Construct stream format
+ return ((is_pcm ? 0 : 1) << 15)
+ | ((selected_sample_rate->base & 0x1) << 14)
+ | ((selected_sample_rate->multiple & 0x7) << 11)
+ | ((selected_sample_rate->divisor & 0x7) << 8)
+ | ((selected_bit_rate->encoding & 0x7) << 4)
+ | ((format.number_of_channels - 1) & 0xf);
+}
+
+ErrorOr<FormatParameters> decode_format(u16 format)
+{
+ // 3.3.41: Input/Output/Bidirectional Stream Descriptor Format
+ // 3.7.1: Stream Format Structure
+
+ // Sample rate
+ u8 sample_rate_base = (format >> 14) & 0x1;
+ u8 sample_rate_multiple = (format >> 11) & 0x7;
+ u8 sample_rate_divisor = (format >> 8) & 0x7;
+ Optional<SampleRateParameters> found_sample_rate {};
+ for (auto sample_rate_parameter : sample_rate_parameters) {
+ if (sample_rate_parameter.base == sample_rate_base
+ && sample_rate_parameter.multiple == sample_rate_multiple
+ && sample_rate_parameter.divisor == sample_rate_divisor) {
+ found_sample_rate = sample_rate_parameter;
+ break;
+ }
+ }
+
+ // PCM bits
+ u8 pcm_bits = (format >> 4) & 0x7;
+ Optional<PcmBitsParameters> found_pcm_bits {};
+ for (auto pcm_bits_parameter : pcm_bits_parameters) {
+ if (pcm_bits_parameter.encoding == pcm_bits) {
+ found_pcm_bits = pcm_bits_parameter;
+ break;
+ }
+ }
+
+ // Number of channels
+ u8 number_of_channels = (format & 0xf) + 1;
+
+ if (!found_sample_rate.has_value() || !found_pcm_bits.has_value())
+ return EINVAL;
+
+ return FormatParameters {
+ .sample_rate = found_sample_rate.release_value().sample_rate,
+ .pcm_bits = found_pcm_bits.release_value().pcm_bits,
+ .number_of_channels = number_of_channels,
+ };
+}
+
+}
diff --git a/Kernel/Devices/Audio/IntelHDA/Format.h b/Kernel/Devices/Audio/IntelHDA/Format.h
new file mode 100644
index 0000000000..df2d8b43f5
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/Format.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Error.h>
+#include <AK/Types.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+struct FormatParameters {
+ u32 sample_rate;
+ u8 pcm_bits;
+ u8 number_of_channels;
+};
+
+ErrorOr<u16> encode_format(FormatParameters format);
+ErrorOr<FormatParameters> decode_format(u16 format);
+
+}
diff --git a/Kernel/Devices/Audio/IntelHDA/OutputPath.h b/Kernel/Devices/Audio/IntelHDA/OutputPath.h
new file mode 100644
index 0000000000..90ff28f6e5
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/OutputPath.h
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/NonnullOwnPtr.h>
+#include <AK/NonnullRefPtr.h>
+#include <AK/StringBuilder.h>
+#include <AK/Vector.h>
+#include <Kernel/Devices/Audio/Channel.h>
+#include <Kernel/Devices/Audio/IntelHDA/Stream.h>
+#include <Kernel/KString.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+class WidgetNode;
+
+class OutputPath {
+public:
+ static constexpr u8 fixed_pcm_bits = 16;
+ static constexpr u8 fixed_channel_count = 2;
+
+ static ErrorOr<NonnullOwnPtr<OutputPath>> create(Vector<NonnullRefPtr<WidgetNode>> widget_path, NonnullOwnPtr<OutputStream> output_stream)
+ {
+ return adopt_nonnull_own_or_enomem(new (nothrow) OutputPath(move(widget_path), move(output_stream)));
+ }
+
+ OutputStream& output_stream() { return *m_output_stream; }
+
+ ErrorOr<void> activate()
+ {
+ // Power on the function group and all widgets that support it
+ auto output_widget = get<WidgetNode::WidgetType::AudioOutput>();
+ auto group = output_widget->parent_node();
+ TRY(group->set_power_state(Node::PowerState::D0));
+ for (auto& widget : m_widget_path) {
+ if (widget->power_control_supported())
+ TRY(widget->set_power_state(Node::PowerState::D0));
+ }
+
+ // Link the audio output widget to the output stream number and first channel
+ TRY(output_widget->set_converter_stream_and_channel(m_output_stream->stream_number(), OutputStream::fixed_channel));
+
+ // Set full volume for all output amplifiers in the path
+ for (auto& widget : m_widget_path) {
+ if (!widget->output_amp_present())
+ continue;
+
+ // NOTE: setting gain to the offset means 0dB attenuation / 100% volume
+ TRY(widget->set_amplifier_gain_mute({
+ .mute = false,
+ .gain = widget->output_amp_capabilities().offset,
+ }));
+ }
+
+ // Walk through pairs of widgets and connect them to each other
+ for (size_t i = 0; i < m_widget_path.size() - 1; ++i) {
+ auto left_widget = m_widget_path[i];
+ auto right_widget = m_widget_path[i + 1];
+
+ VERIFY(left_widget->connection_list_present());
+ if (left_widget->connection_list().size() == 1) {
+ // If there is only one possible connection, it is fixed and we cannot change it.
+ VERIFY(left_widget->connection_selected_node_id() == right_widget->node_id());
+ } else {
+ // Find the index of the right widget node id in the connection list
+ size_t connection_index = 0;
+ for (auto connection_node_id : left_widget->connection_list()) {
+ if (connection_node_id == right_widget->node_id())
+ break;
+ ++connection_index;
+ }
+ VERIFY(connection_index < left_widget->connection_list().size());
+
+ // Select this index
+ TRY(left_widget->set_connection_select(connection_index));
+ }
+ }
+
+ // Enable pin complex output
+ auto pin_widget = get<WidgetNode::WidgetType::PinComplex>();
+ TRY(pin_widget->set_pin_control({ .output_enabled = true }));
+
+ // Finally, retrieve the active converter format for the output widget and set the same for our output stream
+ auto converter_format = TRY(output_widget->get_converter_format());
+ TRY(set_format(converter_format));
+ return {};
+ }
+
+ ErrorOr<void> set_format(FormatParameters format)
+ {
+ // FIXME: support other PCM bit sizes and channel counts
+ format.pcm_bits = fixed_pcm_bits;
+ format.number_of_channels = fixed_channel_count;
+
+ // 7.3.3.8: Converter Format
+ // "The Converter Format control determines the format the converter will use. This must match the
+ // format programmed into the Stream Descriptor on the controller so that the data format being
+ // transmitted on the link matches what is expected by the consumer of the data."
+ auto output_widget = get<WidgetNode::WidgetType::AudioOutput>();
+ if (!output_widget->supported_pcm_rates().contains_slow(format.sample_rate)
+ || !output_widget->supported_pcm_sizes().contains_slow(format.pcm_bits)
+ || format.number_of_channels > output_widget->channel_count())
+ return ENOTSUP;
+
+ TRY(m_output_stream->set_format(format));
+ TRY(output_widget->set_converter_format(format));
+ return {};
+ }
+
+ ErrorOr<NonnullOwnPtr<KString>> to_string()
+ {
+ StringBuilder builder;
+ TRY(builder.try_append("OutputPath: ["sv));
+ for (size_t i = 0; i < m_widget_path.size(); ++i) {
+ auto widget = m_widget_path[i];
+ TRY(builder.try_append(TRY(widget->to_string())->view()));
+ if (i < m_widget_path.size() - 1)
+ TRY(builder.try_append(" โ†’ "sv));
+ }
+ TRY(builder.try_append(']'));
+ return KString::try_create(builder.string_view());
+ }
+
+private:
+ OutputPath(Vector<NonnullRefPtr<WidgetNode>> widget_path, NonnullOwnPtr<OutputStream> output_stream)
+ : m_widget_path(move(widget_path))
+ , m_output_stream(move(output_stream))
+ {
+ }
+
+ template<WidgetNode::WidgetType T>
+ NonnullRefPtr<WidgetNode> get()
+ {
+ for (auto& widget : m_widget_path) {
+ if (widget->widget_type() == T)
+ return widget;
+ }
+ VERIFY_NOT_REACHED();
+ }
+
+ Vector<NonnullRefPtr<WidgetNode>> m_widget_path;
+ NonnullOwnPtr<OutputStream> m_output_stream;
+};
+
+}
diff --git a/Kernel/Devices/Audio/IntelHDA/RingBuffer.h b/Kernel/Devices/Audio/IntelHDA/RingBuffer.h
new file mode 100644
index 0000000000..cb269ea552
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/RingBuffer.h
@@ -0,0 +1,250 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <Kernel/Devices/Audio/IntelHDA/Timing.h>
+#include <Kernel/IOWindow.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+enum class RingBufferType {
+ Input,
+ Output,
+};
+
+// 4.4.1, 4.4.2: CORB and RIRB
+template<typename T, RingBufferType U>
+class ControllerRingBuffer {
+public:
+ ControllerRingBuffer(size_t capacity, NonnullOwnPtr<Memory::Region> buffer, NonnullOwnPtr<IOWindow> register_window)
+ : m_capacity(capacity)
+ , m_buffer(move(buffer))
+ , m_register_window(move(register_window))
+ {
+ // 3.3.22, 3.3.29: Read DMA engine running bit
+ u8 control = m_register_window->read8(RingBufferRegisterOffset::Control);
+ m_running = (control & RingBufferControlFlag::DMAEnable) > 0;
+ }
+
+ static ErrorOr<NonnullOwnPtr<ControllerRingBuffer>> create(StringView name, NonnullOwnPtr<IOWindow> register_window)
+ {
+ // 3.3.24, 3.3.31: Read the size capability
+ auto buffer_size = register_window->read8(RingBufferRegisterOffset::Size);
+ u8 size_capability = buffer_size >> 4;
+ size_t capacity = ((size_capability & SizeCapabilityFlag::Supports2) > 0) ? 2 : 0;
+ if ((size_capability & SizeCapabilityFlag::Supports16) > 0)
+ capacity = 16;
+ if ((size_capability & SizeCapabilityFlag::Supports256) > 0)
+ capacity = 256;
+ if (capacity == 0)
+ return Error::from_string_view_or_print_error_and_return_errno("RingBuffer reports invalid capacity"sv, ENOTSUP);
+
+ // Create a DMA buffer page to holds the ring buffer
+ VERIFY(PAGE_SIZE >= capacity * sizeof(T));
+ auto buffer_region = TRY(MM.allocate_dma_buffer_page(name, U == RingBufferType::Input ? Memory::Region::Access::Read : Memory::Region::Access::Write));
+
+ // 4.4.1.1, 4.4.2: The CORB buffer in memory must be allocated to start on a 128-byte boundary
+ // and in memory configured to match the access type being used.
+ VERIFY((buffer_region->physical_page(0)->paddr().get() & 0x7f) == 0);
+
+ return adopt_nonnull_own_or_enomem(new (nothrow) ControllerRingBuffer(capacity, move(buffer_region), move(register_window)));
+ }
+
+ size_t capacity() const { return m_capacity; }
+
+ ErrorOr<Optional<T>> read_value()
+ requires(U == RingBufferType::Input)
+ {
+ // 4.4.2: Response Inbound Ring Buffer - RIRB
+ auto write_pointer = controller_pointer();
+ dbgln_if(INTEL_HDA_DEBUG, "ControllerRingBuffer({}) {}: current_pointer {} write_pointer {}",
+ U == RingBufferType::Input ? "input"sv : "output"sv, __FUNCTION__, m_current_pointer, write_pointer);
+ if (m_current_pointer == write_pointer)
+ return Optional<T> {};
+
+ m_current_pointer = (m_current_pointer + 1) % m_capacity;
+ return Optional<T> { *(reinterpret_cast<T*>(m_buffer->vaddr().get()) + m_current_pointer) };
+ }
+
+ // 4.4.1.3, 4.4.2.2: Initializing the CORB/RIRB
+ ErrorOr<void> register_with_controller()
+ {
+ // 4.4.1.3, 4.4.2.2: Stop DMA engine
+ TRY(set_dma_engine_running(false));
+
+ // 3.3.18, 3.3.19, 3.3.25, 3.3.26, 4.4.1.3: Set base address
+ PhysicalPtr buffer_address = m_buffer->physical_page(0)->paddr().get();
+ m_register_window->write32(RingBufferRegisterOffset::LowerBaseAddress, buffer_address & 0xffffff80u);
+ if constexpr (sizeof(PhysicalPtr) == 8)
+ m_register_window->write32(RingBufferRegisterOffset::UpperBaseAddress, buffer_address >> 32);
+
+ // 3.3.24, 3.3.31, 4.4.1.3: Set buffer capacity if more than one capacity is supported
+ auto buffer_size = m_register_window->read8(RingBufferRegisterOffset::Size) & static_cast<u8>(~0b11);
+ u8 size_capability = buffer_size >> 4;
+ if (popcount(size_capability) > 1) {
+ switch (m_capacity) {
+ case 2:
+ break;
+ case 16:
+ buffer_size |= 0b01;
+ break;
+ case 256:
+ buffer_size |= 0b10;
+ break;
+ default:
+ VERIFY_NOT_REACHED();
+ }
+ m_register_window->write8(RingBufferRegisterOffset::Size, buffer_size);
+ }
+
+ // 4.4.1.3: Reset read and write pointers to 0
+ TRY(reset_controller_pointer());
+ if constexpr (U == RingBufferType::Output)
+ set_write_pointer(0);
+
+ // FIXME: Qemu's Intel HDA device compares the RINTCNT register with the number of responses sent, even
+ // if interrupts are disabled. This is a workaround and allows us to receive 255 responses. We
+ // should try to fix this upstream or toggle this fix with device quirks logic.
+ if constexpr (U == RingBufferType::Input)
+ m_register_window->write16(RingBufferRegisterOffset::ResponseInterruptCount, 0xff);
+
+ TRY(set_dma_engine_running(true));
+
+ return {};
+ }
+
+ ErrorOr<void> write_value(T value)
+ requires(U == RingBufferType::Output)
+ {
+ // 4.4.1.4: Transmitting Commands via the CORB
+ auto read_pointer = controller_pointer();
+ auto write_pointer = (m_current_pointer + 1) % m_capacity;
+ dbgln_if(INTEL_HDA_DEBUG, "ControllerRingBuffer({}) {}: read_pointer {} write_pointer {}",
+ U == RingBufferType::Input ? "input"sv : "output"sv, __FUNCTION__, read_pointer, write_pointer);
+
+ if (write_pointer == read_pointer)
+ return ENOSPC;
+
+ auto* target_slot = reinterpret_cast<T*>(m_buffer->vaddr().get()) + write_pointer;
+ *target_slot = value;
+ set_write_pointer(write_pointer);
+ return {};
+ }
+
+private:
+ // 3.3: High Definition Audio Controller Register Set - CORB/RIRB
+ enum RingBufferRegisterOffset : u8 {
+ LowerBaseAddress = 0x0,
+ UpperBaseAddress = 0x4,
+ WritePointer = 0x8,
+ ReadPointer = 0xa,
+ ResponseInterruptCount = 0xa,
+ Control = 0xc,
+ Status = 0xd,
+ Size = 0xe,
+ };
+
+ // 3.3.21, 3.3.27: Read/Write Pointer
+ enum PointerFlag : u16 {
+ Reset = 1u << 15,
+ };
+
+ // 3.3.22, 3.3.29: Ring Buffer Control
+ enum RingBufferControlFlag : u8 {
+ DMAEnable = 1u << 1,
+ };
+
+ // 3.3.24, 3.3.31: Size
+ enum SizeCapabilityFlag : u8 {
+ Supports2 = 1u << 0,
+ Supports16 = 1u << 1,
+ Supports256 = 1u << 2,
+ };
+
+ u8 controller_pointer()
+ {
+ // 3.3.21, 3.3.27: Get the Read/Write pointer
+ auto offset = U == RingBufferType::Input
+ ? RingBufferRegisterOffset::WritePointer
+ : RingBufferRegisterOffset::ReadPointer;
+ return m_register_window->read16(offset) & 0xffu;
+ }
+
+ ErrorOr<void> reset_controller_pointer()
+ {
+ // 3.3.21, 3.3.27: Set the Read/Write pointer reset bit
+ auto offset = U == RingBufferType::Input
+ ? RingBufferRegisterOffset::WritePointer
+ : RingBufferRegisterOffset::ReadPointer;
+ m_register_window->write16(offset, PointerFlag::Reset);
+
+ if constexpr (U == RingBufferType::Output) {
+ // 3.3.21: "The hardware will physically update this bit to 1 when the CORB pointer reset is
+ // complete. Software must read a 1 to verify that the reset completed correctly."
+ TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() {
+ u16 read_pointer = m_register_window->read16(offset);
+ return (read_pointer & PointerFlag::Reset) > 0;
+ }));
+
+ // 3.3.21: "Software must clear this bit back to 0, by writing a 0, and then read back the 0
+ // to verify that the clear completed correctly."
+ m_register_window->write16(offset, 0);
+ TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() {
+ u16 read_pointer = m_register_window->read16(offset);
+ return (read_pointer & PointerFlag::Reset) == 0;
+ }));
+ }
+
+ dbgln_if(INTEL_HDA_DEBUG, "ControllerRingBuffer({}) {}",
+ U == RingBufferType::Input ? "input"sv : "output"sv, __FUNCTION__);
+
+ return {};
+ }
+
+ ErrorOr<void> set_dma_engine_running(bool running)
+ {
+ if (m_running == running)
+ return {};
+
+ // 3.3.22, 3.3.29: Set DMA engine running bit
+ u8 control = m_register_window->read8(RingBufferRegisterOffset::Control);
+ if (running)
+ control |= RingBufferControlFlag::DMAEnable;
+ else
+ control &= ~RingBufferControlFlag::DMAEnable;
+ dbgln_if(INTEL_HDA_DEBUG, "ControllerRingBuffer({}) {}: {:#08b}",
+ U == RingBufferType::Input ? "input"sv : "output"sv, __FUNCTION__, control);
+ m_register_window->write8(RingBufferRegisterOffset::Control, control);
+
+ // Must read the value back
+ TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() {
+ control = m_register_window->read8(RingBufferRegisterOffset::Control);
+ return (control & RingBufferControlFlag::DMAEnable) == (running ? RingBufferControlFlag::DMAEnable : 0);
+ }));
+ m_running = running;
+ return {};
+ }
+
+ void set_write_pointer(u8 pointer)
+ requires(U == RingBufferType::Output)
+ {
+ // 3.3.20: CORBWP โ€“ CORB Write Pointer
+ m_register_window->write16(RingBufferRegisterOffset::WritePointer, pointer);
+ m_current_pointer = pointer;
+ }
+
+ size_t m_capacity;
+ NonnullOwnPtr<Memory::Region> m_buffer;
+ NonnullOwnPtr<IOWindow> m_register_window;
+ bool m_running { false };
+ u8 m_current_pointer { 0 };
+};
+
+using CommandOutboundRingBuffer = ControllerRingBuffer<u32, RingBufferType::Output>;
+using ResponseInboundRingBuffer = ControllerRingBuffer<u64, RingBufferType::Input>;
+
+}
diff --git a/Kernel/Devices/Audio/IntelHDA/Stream.cpp b/Kernel/Devices/Audio/IntelHDA/Stream.cpp
new file mode 100644
index 0000000000..52f8a886ae
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/Stream.cpp
@@ -0,0 +1,249 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "Stream.h"
+
+#include <AK/Optional.h>
+#include <Kernel/Devices/Audio/IntelHDA/Controller.h>
+#include <Kernel/Devices/Audio/IntelHDA/Format.h>
+#include <Kernel/Devices/Audio/IntelHDA/Timing.h>
+#include <Kernel/Memory/MemoryManager.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+Stream::~Stream()
+{
+ if (m_running)
+ MUST(stop());
+}
+
+u32 Stream::read_control()
+{
+ // 3.3.35: Input/Output/Bidirectional Stream Descriptor Control
+ u32 control_and_status = m_stream_io_window->read32(StreamRegisterOffset::Control);
+ return control_and_status & 0xffffffu;
+}
+
+void Stream::write_control(u32 control)
+{
+ // 3.3.35: Input/Output/Bidirectional Stream Descriptor Control
+ auto status = m_stream_io_window->read8(StreamRegisterOffset::Status);
+ u32 control_and_status = (status << 24)
+ | ((m_stream_number & 0xf) << 20)
+ | (control & 0xfffffu);
+ m_stream_io_window->write32(StreamRegisterOffset::Control, control_and_status);
+}
+
+static constexpr u8 container_size_in_bytes(u8 bit_size)
+{
+ // 4.5.1: Stream Data In Memory
+ if (bit_size > 16)
+ return 4;
+ else if (bit_size > 8)
+ return 2;
+ return 1;
+}
+
+ErrorOr<void> Stream::initialize_buffer()
+{
+ VERIFY(m_format_parameters.sample_rate > 0);
+ VERIFY(m_format_parameters.pcm_bits > 0);
+ VERIFY(m_format_parameters.number_of_channels > 0);
+
+ // 4.5.1: Stream Data In Memory
+ // NOTE: we ignore the number of blocks per packet since we are only required to have an integer number
+ // of samples per buffer, and we always have at least one packet per buffer.
+ size_t block_size_in_bytes = container_size_in_bytes(m_format_parameters.pcm_bits) * m_format_parameters.number_of_channels;
+ size_t number_of_blocks_in_buffer = PAGE_SIZE / block_size_in_bytes;
+ VERIFY(number_of_blocks_in_buffer > 0);
+
+ size_t number_of_blocks_required_for_cyclic_buffer_size = ceil_div(cyclic_buffer_size_in_ms * m_format_parameters.sample_rate, 1'000);
+ size_t number_of_buffers_required_for_cyclic_buffer_size = AK::max(ceil_div(number_of_blocks_required_for_cyclic_buffer_size, number_of_blocks_in_buffer), minimum_number_of_buffers);
+ VERIFY(number_of_buffers_required_for_cyclic_buffer_size > 0 && number_of_buffers_required_for_cyclic_buffer_size <= 256);
+
+ size_t cyclic_buffer_size_in_bytes = number_of_buffers_required_for_cyclic_buffer_size * PAGE_SIZE;
+
+ TRY(m_buffers.with([&](auto& buffers) -> ErrorOr<void> {
+ buffers = TRY(MM.allocate_dma_buffer_pages(cyclic_buffer_size_in_bytes, "IntelHDA Stream Buffers"sv, Memory::Region::Access::ReadWrite));
+
+ // 3.3.38 Input/Output/Bidirectional Stream Descriptor Cyclic Buffer Length
+ m_stream_io_window->write32(StreamRegisterOffset::CyclicBufferLength, buffers->size());
+
+ // 3.3.39: Input/Output/Bidirectional Stream Descriptor Last Valid Index
+ VERIFY(number_of_buffers_required_for_cyclic_buffer_size <= 256);
+ m_stream_io_window->write16(StreamRegisterOffset::LastValidIndex, number_of_buffers_required_for_cyclic_buffer_size - 1);
+
+ // 3.6.2: Buffer Descriptor List
+ m_buffer_descriptor_list = TRY(MM.allocate_dma_buffer_page("IntelHDA Stream BDL"sv, Memory::Region::Access::ReadWrite));
+ auto bdl_physical_address = m_buffer_descriptor_list->physical_page(0)->paddr().get();
+ m_stream_io_window->write32(StreamRegisterOffset::BDLLowerBaseAddress, bdl_physical_address & 0xffffffffu);
+ m_stream_io_window->write32(StreamRegisterOffset::BDLUpperBaseAddress, bdl_physical_address >> 32);
+
+ // 3.6.3: Buffer Descriptor List Entry
+ auto* buffer_descriptor_entry = m_buffer_descriptor_list->vaddr().as_ptr();
+ for (u8 buffer_index = 0; buffer_index < buffers->page_count(); ++buffer_index) {
+ auto* entry = buffer_descriptor_entry + buffer_index * 0x10;
+ *bit_cast<u64*>(entry) = buffers->physical_page(buffer_index)->paddr().get();
+ *bit_cast<u32*>(entry + 8) = PAGE_SIZE;
+ *bit_cast<u32*>(entry + 12) = 0;
+ }
+ return {};
+ }));
+ return {};
+}
+
+ErrorOr<void> Stream::reset()
+{
+ // 3.3.35: Input/Output/Bidirectional Stream Descriptor Control
+ if (m_running)
+ TRY(stop());
+
+ // Writing a 1 causes the corresponding stream to be reset. The Stream Descriptor registers
+ // (except the SRST bit itself), FIFO's, and cadence generator for the corresponding stream
+ // are reset.
+ auto control = read_control();
+ control |= StreamControlFlag::StreamReset;
+ write_control(control);
+
+ // After the stream hardware has completed sequencing into the reset state, it will report a
+ // 1 in this bit. Software must read a 1 from this bit to verify that the stream is in reset.
+ TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() {
+ control = read_control();
+ return (control & StreamControlFlag::StreamReset) > 0;
+ }));
+
+ // Writing a 0 causes the corresponding stream to exit reset.
+ control &= ~StreamControlFlag::StreamReset;
+ write_control(control);
+
+ // When the stream hardware is ready to begin operation, it will report a 0 in this bit.
+ // Software must read a 0 from this bit before accessing any of the stream registers
+ return wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() {
+ control = read_control();
+ return (control & StreamControlFlag::StreamReset) == 0;
+ });
+}
+
+void Stream::start()
+{
+ // 3.3.35: Input/Output/Bidirectional Stream Descriptor Control
+ VERIFY(!m_running);
+ dbgln_if(INTEL_HDA_DEBUG, "IntelHDA: Starting stream");
+
+ auto control = read_control();
+ control |= StreamControlFlag::StreamRun;
+ write_control(control);
+ m_running = true;
+}
+
+ErrorOr<void> Stream::stop()
+{
+ // 3.3.35: Input/Output/Bidirectional Stream Descriptor Control
+ VERIFY(m_running);
+ dbgln_if(INTEL_HDA_DEBUG, "IntelHDA: Stopping stream");
+
+ auto control = read_control();
+ control &= ~StreamControlFlag::StreamRun;
+ write_control(control);
+
+ // 4.5.4: Stopping Streams
+ // Wait until RUN bit is 0
+ TRY(wait_until(frame_delay_in_microseconds(1), controller_timeout_in_microseconds, [&]() {
+ control = read_control();
+ return (control & StreamControlFlag::StreamRun) == 0;
+ }));
+
+ m_running = false;
+ m_buffer_position = 0;
+ return {};
+}
+
+ErrorOr<void> Stream::set_format(FormatParameters format)
+{
+ // Reset the stream so we can set a new buffer
+ TRY(reset());
+
+ // Write the sample rate payload
+ auto format_payload = TRY(encode_format(format));
+ m_stream_io_window->write16(StreamRegisterOffset::Format, format_payload);
+ m_format_parameters = format;
+
+ // Re-initialize the bufer
+ TRY(initialize_buffer());
+ return {};
+}
+
+ErrorOr<size_t> OutputStream::write(UserOrKernelBuffer const& data, size_t length)
+{
+ auto wait_until_buffer_index_can_be_written = [&](u8 buffer_index) {
+ while (m_running) {
+ auto link_position = m_stream_io_window->read32(StreamRegisterOffset::LinkPosition);
+ auto read_buffer_index = link_position / PAGE_SIZE;
+ if (read_buffer_index != buffer_index)
+ return;
+
+ auto microseconds_to_wait = ((read_buffer_index + 1) * PAGE_SIZE - link_position)
+ / m_format_parameters.number_of_channels
+ * 8 / m_format_parameters.pcm_bits
+ * 1'000'000 / m_format_parameters.sample_rate;
+ dbgln_if(INTEL_HDA_DEBUG, "IntelHDA: Waiting {} ยตs until buffer {} becomes writeable", microseconds_to_wait, buffer_index);
+
+ // NOTE: we don't care about the reason for interruption - we simply calculate the next delay
+ [[maybe_unused]] auto block_result = Thread::current()->sleep(Time::from_microseconds(microseconds_to_wait));
+ }
+ };
+
+ auto write_into_single_buffer = [&](UserOrKernelBuffer const& data, size_t data_offset, size_t length, size_t offset_within_buffer) -> ErrorOr<u8> {
+ u8 buffer_index = m_buffer_position / PAGE_SIZE;
+ VERIFY(length <= PAGE_SIZE - offset_within_buffer);
+
+ wait_until_buffer_index_can_be_written(buffer_index);
+
+ TRY(m_buffers.with([&](auto& buffers) -> ErrorOr<void> {
+ // NOTE: if the buffers were reinitialized, we might point to an out of bounds page
+ if (buffer_index >= buffers->page_count())
+ return EAGAIN;
+
+ auto* buffer = buffers->vaddr_from_page_index(buffer_index).as_ptr() + offset_within_buffer;
+ TRY(data.read(buffer, data_offset, length));
+
+ // Cycle back to position 0 when we reach the end
+ m_buffer_position += length;
+ VERIFY(m_buffer_position <= buffers->size());
+ if (m_buffer_position == buffers->size())
+ m_buffer_position = 0;
+ return {};
+ }));
+ return buffer_index;
+ };
+
+ // FIXME: support PCM bit sizes other than 16
+ VERIFY(m_format_parameters.pcm_bits == 16);
+
+ // Split up input data into separate buffer writes
+ size_t length_remaining = length;
+ size_t data_offset = 0;
+ u8 last_buffer_index = 0;
+ while (length_remaining > 0) {
+ size_t offset_within_current_buffer = m_buffer_position % PAGE_SIZE;
+ size_t length_to_write = AK::min(length_remaining, PAGE_SIZE - offset_within_current_buffer);
+
+ last_buffer_index = TRY(write_into_single_buffer(data, data_offset, length_to_write, offset_within_current_buffer));
+
+ data_offset += length_to_write;
+ length_remaining -= length_to_write;
+ }
+
+ // Start this stream if not already running
+ // 3.3.39: LVI must be at least 1; i.e., there must be at least two valid entries in
+ // the buffer descriptor list before DMA operations can begin.
+ if (!m_running && last_buffer_index >= 2)
+ start();
+
+ return length;
+}
+
+}
diff --git a/Kernel/Devices/Audio/IntelHDA/Stream.h b/Kernel/Devices/Audio/IntelHDA/Stream.h
new file mode 100644
index 0000000000..eb61165a19
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/Stream.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Error.h>
+#include <AK/NonnullOwnPtr.h>
+#include <AK/OwnPtr.h>
+#include <Kernel/Devices/Audio/IntelHDA/Codec.h>
+#include <Kernel/Devices/Audio/IntelHDA/Format.h>
+#include <Kernel/IOWindow.h>
+#include <Kernel/Locking/SpinlockProtected.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+class Stream {
+public:
+ static constexpr u32 cyclic_buffer_size_in_ms = 40;
+
+ u8 stream_number() const { return m_stream_number; }
+ bool running() const { return m_running; }
+ u32 sample_rate() const { return m_format_parameters.sample_rate; }
+
+ void start();
+ ErrorOr<void> stop();
+
+ ErrorOr<void> set_format(FormatParameters);
+
+protected:
+ // We always need 2 filled buffers, plus an additional one to prevent buffer underrun
+ static constexpr u8 minimum_number_of_buffers = 3;
+
+ // 3.3: High Definition Audio Controller Register Set - streams
+ enum StreamRegisterOffset : u8 {
+ Control = 0x00,
+ Status = 0x03,
+ LinkPosition = 0x04,
+ CyclicBufferLength = 0x08,
+ LastValidIndex = 0x0c,
+ Format = 0x12,
+ BDLLowerBaseAddress = 0x18,
+ BDLUpperBaseAddress = 0x1c,
+ };
+
+ // 3.3.35: Input/Output/Bidirectional Stream Descriptor Control
+ enum StreamControlFlag : u32 {
+ StreamReset = 1u << 0,
+ StreamRun = 1u << 1,
+ };
+
+ Stream(NonnullOwnPtr<IOWindow> stream_io_window, u8 stream_number)
+ : m_stream_io_window(move(stream_io_window))
+ , m_stream_number(stream_number)
+ {
+ }
+
+ ~Stream();
+
+ u32 read_control();
+ void write_control(u32);
+
+ ErrorOr<void> initialize_buffer();
+ ErrorOr<void> reset();
+
+ NonnullOwnPtr<IOWindow> m_stream_io_window;
+ u8 m_stream_number;
+ OwnPtr<Memory::Region> m_buffer_descriptor_list;
+ SpinlockProtected<OwnPtr<Memory::Region>, LockRank::None> m_buffers;
+ size_t m_buffer_position { 0 };
+ bool m_running { false };
+ FormatParameters m_format_parameters;
+};
+
+class OutputStream : public Stream {
+public:
+ static constexpr u8 fixed_channel = 0;
+
+ static ErrorOr<NonnullOwnPtr<OutputStream>> create(NonnullOwnPtr<IOWindow> stream_io_window, u8 stream_number)
+ {
+ return adopt_nonnull_own_or_enomem(new (nothrow) OutputStream(move(stream_io_window), stream_number));
+ }
+
+ ErrorOr<size_t> write(UserOrKernelBuffer const&, size_t);
+
+private:
+ OutputStream(NonnullOwnPtr<IOWindow> stream_io_window, u8 stream_number)
+ : Stream(move(stream_io_window), stream_number)
+ {
+ // 3.3.35: Input/Output/Bidirectional Stream Descriptor Control
+ // "Although the controller hardware is capable of transmitting any stream number,
+ // by convention stream 0 is reserved as unused by software, so that converters
+ // whose stream numbers have been reset to 0 do not unintentionally decode data
+ // not intended for them."
+ VERIFY(stream_number >= 1);
+ }
+};
+
+// FIXME: implement InputStream and BidirectionalStream
+
+}
diff --git a/Kernel/Devices/Audio/IntelHDA/Timing.h b/Kernel/Devices/Audio/IntelHDA/Timing.h
new file mode 100644
index 0000000000..0c8db54e57
--- /dev/null
+++ b/Kernel/Devices/Audio/IntelHDA/Timing.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023, Jelle Raaijmakers <jelle@gmta.nl>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Error.h>
+#include <AK/Function.h>
+
+namespace Kernel::Audio::IntelHDA {
+
+// We define an arbitrary controller timeout of 300ms for most actions.
+constexpr size_t controller_timeout_in_microseconds = 300'000;
+
+consteval u32 frame_delay_in_microseconds(u32 frames)
+{
+ // NOTE: the link operates at this _fixed_ frequency and is independent of the streams' rates.
+ constexpr u32 link_frame_frequency_hz = 48'000;
+
+ // 2.2: Streams and Channels
+ // A new frame starts exactly every 20.83 ฮผs, corresponding to the common 48-kHz sample rate.
+ VERIFY(frames <= 4294);
+ return frames * 1'000'000u / link_frame_frequency_hz + 1u;
+}
+
+ErrorOr<void> wait_until(size_t delay_in_microseconds, size_t timeout_in_microseconds, Function<ErrorOr<bool>()> condition);
+
+}
diff --git a/Kernel/Devices/Audio/Management.cpp b/Kernel/Devices/Audio/Management.cpp
index abf57a4991..eac2834791 100644
--- a/Kernel/Devices/Audio/Management.cpp
+++ b/Kernel/Devices/Audio/Management.cpp
@@ -8,6 +8,7 @@
#include <Kernel/Bus/PCI/API.h>
#include <Kernel/Bus/PCI/IDs.h>
#include <Kernel/Devices/Audio/AC97.h>
+#include <Kernel/Devices/Audio/IntelHDA/Controller.h>
#include <Kernel/Devices/Audio/Management.h>
#include <Kernel/Sections.h>
@@ -38,23 +39,32 @@ UNMAP_AFTER_INIT AudioManagement::AudioManagement()
UNMAP_AFTER_INIT void AudioManagement::enumerate_hardware_controllers()
{
- if (!PCI::Access::is_disabled()) {
- MUST(PCI::enumerate([&](PCI::DeviceIdentifier const& device_identifier) {
- // Note: Only consider PCI audio controllers
- if (device_identifier.class_code().value() != to_underlying(PCI::ClassID::Multimedia)
- || device_identifier.subclass_code().value() != to_underlying(PCI::Multimedia::SubclassID::AudioController))
- return;
+ if (PCI::Access::is_disabled())
+ return;
+ MUST(PCI::enumerate([&](PCI::DeviceIdentifier const& device_identifier) {
+ // Only consider PCI multimedia devices
+ if (device_identifier.class_code().value() != to_underlying(PCI::ClassID::Multimedia))
+ return;
- dbgln("AC97: found audio controller at {}", device_identifier.address());
- auto ac97_device = AC97::try_create(device_identifier);
- if (ac97_device.is_error()) {
- // FIXME: Propagate errors properly
- dbgln("AudioManagement: failed to initialize AC97 device: {}", ac97_device.error());
- return;
+ auto create_audio_controller = [](PCI::DeviceIdentifier const& device_identifier) -> ErrorOr<NonnullLockRefPtr<AudioController>> {
+ switch (static_cast<PCI::Multimedia::SubclassID>(device_identifier.subclass_code().value())) {
+ case PCI::Multimedia::SubclassID::AudioController:
+ return AC97::try_create(device_identifier);
+ case PCI::Multimedia::SubclassID::HDACompatibleController:
+ return Audio::IntelHDA::Controller::create(device_identifier);
+ default:
+ return ENOTSUP;
}
- m_controllers_list.append(ac97_device.release_value());
- }));
- }
+ };
+
+ dbgln("AudioManagement: found audio controller {} at {}", device_identifier.hardware_id(), device_identifier.address());
+ auto audio_controller_device = create_audio_controller(device_identifier);
+ if (audio_controller_device.is_error()) {
+ dbgln("AudioManagement: failed to initialize audio controller: {}", audio_controller_device.error());
+ return;
+ }
+ m_controllers_list.append(audio_controller_device.release_value());
+ }));
}
UNMAP_AFTER_INIT void AudioManagement::enumerate_hardware_audio_channels()
@@ -67,16 +77,14 @@ UNMAP_AFTER_INIT bool AudioManagement::initialize()
{
/* Explanation on the flow:
- * 1. Enumerate all audio controllers connected to the system:
- * a. Try to find the SB16 ISA-based controller.
- * b. Enumerate the PCI bus and try to find audio controllers there too
+ * 1. Enumerate the PCI bus and try to find audio controllers
* 2. Ask each controller to detect the audio channels and instantiate AudioChannel objects.
*/
enumerate_hardware_controllers();
enumerate_hardware_audio_channels();
if (m_controllers_list.is_empty()) {
- dbgln("No audio controller was initialized.");
+ dbgln("AudioManagement: no audio controller was initialized.");
return false;
}
return true;