diff --git a/includes/modules/shortcodes/types/audio.php b/includes/modules/shortcodes/types/audio.php
new file mode 100644
index 0000000..7eafb10
--- /dev/null
+++ b/includes/modules/shortcodes/types/audio.php
@@ -0,0 +1,227 @@
+ '',
+ 'attachment_id' => '',
+ 'autoplay' => 'false',
+ 'loop' => 'false',
+ 'muted' => 'false',
+ 'controls' => 'true',
+ 'preload' => 'metadata',
+ 'class' => '',
+ 'id' => '',
+ 'before' => '',
+ 'after' => '',
+ 'fallback' => '',
+ 'format' => '',
+ ];
+ }
+
+ /**
+ * Renders the shortcode.
+ *
+ * @since NEXT
+ *
+ * @param array $attributes Shortcode attributes.
+ * @param string $content Enclosed content (unused).
+ *
+ * @return string
+ */
+ public function render( array $attributes, string $content ): string {
+ // Retrieves attributes merged with defaults.
+ $attributes = $this->get_attributes( $attributes );
+
+ // Parses dynamic attributes.
+ if ( function_exists( 'anys_parse_dynamic_attributes' ) ) {
+ $attributes = anys_parse_dynamic_attributes( $attributes );
+ }
+
+ // Resolves attachment ID to URL when provided.
+ $attachment_id = isset( $attributes['attachment_id'] ) && is_numeric( $attributes['attachment_id'] )
+ ? (int) $attributes['attachment_id']
+ : 0;
+ $attachment_url = $attachment_id > 0 ? wp_get_attachment_url( $attachment_id ) : '';
+
+ if ( $attachment_url ) {
+ $attributes['src'] = $attachment_url;
+ }
+
+ $src = isset( $attributes['src'] ) ? trim( (string) $attributes['src'] ) : '';
+
+ // Stops when no audio source exists.
+ if ( $src === '' ) {
+ return '';
+ }
+
+ // Handles blocked hosts.
+ $is_blocked = $this->is_blocked_audio_host( $src );
+
+ if ( $is_blocked && ! is_admin() ) {
+ return '';
+ }
+
+ if ( $is_blocked && is_admin() ) {
+ return '' .
+ esc_html__( 'Streaming audio URLs are not supported by the audio shortcode.', 'anys' ) .
+ '';
+ }
+
+ $src = esc_url( $src );
+
+ $audio_attributes = [];
+
+ // Adds ID attribute.
+ if ( ! empty( $attributes['id'] ) ) {
+ $audio_attributes[] = 'id="' . esc_attr( $attributes['id'] ) . '"';
+ }
+
+ // Adds class attribute.
+ $classes = [ 'anys-audio' ];
+ if ( ! empty( $attributes['class'] ) ) {
+ $classes[] = trim( (string) $attributes['class'] );
+ }
+ $audio_attributes[] = 'class="' . esc_attr( implode( ' ', $classes ) ) . '"';
+
+ // Adds src attribute.
+ $audio_attributes[] = 'src="' . $src . '"';
+
+ // Adds boolean attributes.
+ foreach ( [ 'controls', 'autoplay', 'loop', 'muted' ] as $bool_key ) {
+ if ( isset( $attributes[ $bool_key ] ) && $this->is_true( $attributes[ $bool_key ] ) ) {
+ $audio_attributes[] = $bool_key;
+ }
+ }
+
+ // Adds preload attribute.
+ if ( isset( $attributes['preload'] ) ) {
+ $preload = strtolower( trim( (string) $attributes['preload'] ) );
+ if ( in_array( $preload, [ 'auto', 'metadata', 'none' ], true ) ) {
+ $audio_attributes[] = 'preload="' . esc_attr( $preload ) . '"';
+ }
+ }
+
+ $attr_string = implode( ' ', $audio_attributes );
+
+ // Builds the audio element.
+ $value = '';
+
+ // Wraps with before/after and fallback.
+ if ( function_exists( 'anys_wrap_output' ) ) {
+ $output = anys_wrap_output( $value, $attributes );
+ } else {
+ $output = $value;
+ }
+
+ // Returns sanitized output.
+ return wp_kses_post( (string) $output );
+ }
+
+ /**
+ * Checks whether the audio URL belongs to a blocked host.
+ *
+ * @since NEXT
+ *
+ * @param string $url Audio URL.
+ *
+ * @return bool
+ */
+ private function is_blocked_audio_host( $url ): bool {
+ $host = wp_parse_url( $url, PHP_URL_HOST );
+
+ if ( ! is_string( $host ) || $host === '' ) {
+ return false;
+ }
+
+ $host = strtolower( $host );
+ $blocked = [
+ 'soundcloud.com',
+ 'www.soundcloud.com',
+ 'spotify.com',
+ 'www.spotify.com',
+ 'open.spotify.com',
+ 'music.apple.com',
+ 'deezer.com',
+ 'www.deezer.com',
+ 'tidal.com',
+ 'www.tidal.com',
+ 'pandora.com',
+ 'www.pandora.com',
+ 'mixcloud.com',
+ 'www.mixcloud.com',
+ 'audiomack.com',
+ 'www.audiomack.com',
+ ];
+
+ foreach ( $blocked as $domain ) {
+ $length = strlen( (string) $domain );
+ $is_exact = ( $host === $domain );
+ $is_sub = $length > 0 && substr( $host, - ( $length + 1 ) ) === '.' . $domain;
+
+ if ( $is_exact || $is_sub ) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Normalizes boolean-like attribute values.
+ *
+ * @since NEXT
+ *
+ * @param mixed $value Raw attribute value.
+ *
+ * @return bool
+ */
+ private function is_true( $value ): bool {
+ if ( is_bool( $value ) ) {
+ return $value;
+ }
+
+ $value = strtolower( trim( (string) $value ) );
+
+ return in_array(
+ $value,
+ [ '1', 'true', 'yes', 'on' ],
+ true
+ );
+ }
+}