Alsa-devel
Threads by month
- ----- 2025 -----
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2010 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2009 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2008 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2007 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- 51470 discussions
alsa-project/alsa-utils issue #307 was opened from MarcWeber:
This a bug report that arecordmidi's metronome doesn't work on my (nixos) linux
machine for whatever reason. Asking AI to fix it I couldn't really make it work.
It kept stopping after one measure.
So I asked to rewrite roughly with the following prompt:
- master / -slave (be metronome or use another time giving source) slave is not tested.
Because goal was to get readable clean nodes I asked to add new features:
--note-recording-on-switches-others-off
--note-recording-ignore-off
But the honest answer is using lmms like tool to record midi shows that getting
timing correct is too hard. So the only way to make this work I feel would be using A UI
app which allows to manually fix the timing of measures. But that would be a
totally different bloated app.
So typical usage of the code below would be:
```
arecordmidi --debug --master \
--note-recording-on-switches-others-off --note-recording-ignore-off \
-p 24:0 -m 128:1 -i 4:4 -t 50 /tmp/out.mid
```
But I still wasn't able to cleanly record "Alle meine Entchen" which only is
made up of quarter half and full note length.
The code below was done by Gemini Pro 2025-10-31 but it was fed with the
original code. I also applied some manual fixes to the AI code cause it kept failing on some things. So if AI is a programmer it probably is still GPL ? No idea.
So maybe test whether the metronome works for you if not add a simple patch
showing this issue (for now) so that people can compile the code below
themselves if they want to experimenfit with it. But they will have read the
warnings here understanding it might not be as easy as they think.
So I am not sure anymore the bug is worth fixing. But this issue allows
discusssing it. But its worth managing expectations of users - thus drop the feature if it doesn't work or fix it by printf ("see this gituhb issue") for the time being.
Maybe the code is old and was written for older Alsa no idea. The metronome output wasn't even shown in qjackctl or aconnect -l. So this all at least works in the code below so that it can be experimented with. It also outputs the pressed notes with timing differences compared to ticks. But in the end you might be faster using lilypond or musescore to type notes.
```
/*
* arecordmidi.c - record standard MIDI files from sequencer
*
* Copyright (c) 2004-2005 Clemens Ladisch <clemens(a)ladisch.de>
* Copyright (c) 2024 Rewritten by AI based on user specification.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <signal.h>
#include <getopt.h>
#include <poll.h>
#include <errno.h>
#include <math.h>
#include <time.h> /* for clock_gettime */
#include <alsa/asoundlib.h>
#include "aconfig.h"
#include "version.h"
#define BUFFER_SIZE 4096
#define MAX_PORTS 16
/* --- Data Structures --- */
// Linked list of buffers to store SMF track data
struct buffer {
struct buffer *next;
unsigned char buf[BUFFER_SIZE];
};
// Represents a single track in a Standard MIDI File
struct smf_track {
int size;
int cur_buf_size;
struct buffer *cur_buf;
snd_seq_tick_time_t last_tick;
unsigned char last_command;
int used;
struct buffer first_buf;
};
#define TRACKS_PER_PORT 17 // 1 for meta/sysex, 16 for channels
// Global application state
typedef struct {
enum { MODE_UNSPECIFIED, MODE_MASTER, MODE_SLAVE } mode;
const char *output_filename;
FILE *file;
snd_seq_t *seq;
int client_id;
int queue_id;
char *record_in_addrs[MAX_PORTS];
int record_in_count;
char *metronome_out_addr;
char *clock_in_addr; // Slave only
char *clock_out_addr; // Master only
int record_in_port_ids[MAX_PORTS];
int metronome_out_port_id;
int metronome_loop_port_id;
int clock_out_port_id;
int bpm;
int ticks_per_beat; // PPQ (Pulses Per Quarter Note)
int use_metronome;
int metro_channel;
int metro_strong_note;
int metro_weak_note;
int metro_velocity;
long metro_latency_ms;
snd_seq_tick_time_t metro_tick_offset;
int metro_running;
int ts_num;
int ts_den;
int ts_den_pow2;
int split_channels;
int num_tracks;
struct smf_track *tracks;
volatile sig_atomic_t stop_request;
int debug_mode;
// New options
int on_switches_others_off;
int ignore_off;
// For note duration calculation in debug mode
snd_seq_tick_time_t active_note_start_tick[MAX_PORTS][16][128];
// For timing reference in debug mode
snd_seq_tick_time_t ref_tick;
snd_seq_real_time_t ref_real_time;
int queue_time_ref_set;
} arecordmidi_app_t;
static arecordmidi_app_t app = {0};
/* --- Error Handling & Memory --- */
static void fatal(const char *msg, ...) {
va_list ap;
va_start(ap, msg);
fputs("Error: ", stderr);
vfprintf(stderr, msg, ap); fflush(stdout);
va_end(ap);
fputc('\n', stderr);
exit(EXIT_FAILURE);
}
static void check_mem(void *p) {
if (!p)
fatal("Out of memory");
}
static void check_snd(int err, const char *operation) {
if (err < 0)
fatal("ALSA Error: Cannot %s - %s", operation, snd_strerror(err));
}
/* --- SMF File Writing Utilities --- */
static void add_byte(struct smf_track *track, unsigned char byte) {
if (track->cur_buf_size >= BUFFER_SIZE) {
track->cur_buf->next = calloc(1, sizeof(struct buffer));
check_mem(track->cur_buf->next);
track->cur_buf = track->cur_buf->next;
track->cur_buf_size = 0;
}
track->cur_buf->buf[track->cur_buf_size++] = byte;
track->size++;
}
static void var_value(struct smf_track *track, unsigned int v) {
unsigned long buffer = v & 0x7f;
while ((v >>= 7) > 0) {
buffer <<= 8;
buffer |= 0x80 | (v & 0x7f);
}
while (1) {
add_byte(track, buffer & 0xff);
if (buffer & 0x80)
buffer >>= 8;
else
break;
}
}
static void delta_time(struct smf_track *track, const snd_seq_event_t *ev) {
int diff = ev->time.tick - track->last_tick;
if (diff < 0) diff = 0;
var_value(track, diff);
track->last_tick = ev->time.tick;
}
static void command(struct smf_track *track, unsigned char cmd) {
if (cmd != track->last_command) {
add_byte(track, cmd);
track->last_command = cmd < 0xf0 ? cmd : 0;
}
}
/* --- Metronome --- */
static void metronome_note(unsigned char note, unsigned int tick) {
snd_seq_event_t ev;
snd_seq_ev_clear(&ev);
snd_seq_ev_set_source(&ev, app.metronome_out_port_id);
snd_seq_ev_set_subs(&ev);
snd_seq_ev_set_direct(&ev);
unsigned int schedule_tick = tick > app.metro_tick_offset ? tick - app.metro_tick_offset : 0;
if (app.debug_mode) {
// printf(" OUT:METRO: Note On: ch=%2d key=%3d vel=%3d @ tick %u\n", app.metro_channel, note, app.metro_velocity, schedule_tick);
}
// Note On
snd_seq_ev_set_noteon(&ev, app.metro_channel, note, app.metro_velocity);
snd_seq_ev_schedule_tick(&ev, app.queue_id, 0, schedule_tick); // Absolute schedule
snd_seq_event_output(app.seq, &ev);
// Note Off (short duration)
snd_seq_ev_set_noteoff(&ev, app.metro_channel, note, 0);
snd_seq_ev_schedule_tick(&ev, app.queue_id, 0, schedule_tick + app.ticks_per_beat / 4); // Absolute
snd_seq_event_output(app.seq, &ev);
}
static void metronome_pattern(unsigned int tick) {
unsigned int ticks_per_bar, t;
int j;
ticks_per_bar = app.ts_num * (app.ticks_per_beat * 4 / app.ts_den);
t = tick;
for (j = 0; j < app.ts_num; ++j) {
metronome_note((j == 0) ? app.metro_strong_note : app.metro_weak_note, t);
t += (app.ticks_per_beat * 4 / app.ts_den);
}
// Schedule a custom event to trigger the next bar
unsigned int next_bar_tick = tick + ticks_per_bar;
snd_seq_event_t ev;
snd_seq_ev_clear(&ev);
ev.type = SND_SEQ_EVENT_USR0;
snd_seq_ev_schedule_tick(&ev, app.queue_id, 0, next_bar_tick); // Absolute schedule
snd_seq_ev_set_source(&ev, app.metronome_loop_port_id);
snd_seq_ev_set_dest(&ev, app.client_id, app.metronome_loop_port_id);
snd_seq_event_output(app.seq, &ev);
snd_seq_drain_output(app.seq);
}
static void start_metronome() {
if (app.metro_running || !app.use_metronome) return;
app.metro_running = 1;
printf("Starting metronome.\n");
// Get current tick to start metronome immediately
snd_seq_queue_status_t *qstatus;
snd_seq_queue_status_alloca(&qstatus);
check_snd(snd_seq_get_queue_status(app.seq, app.queue_id, qstatus), "get queue status");
snd_seq_tick_time_t current_tick = snd_seq_queue_status_get_tick_time(qstatus);
metronome_pattern(current_tick);
}
/* --- Event Recording --- */
static const char *note_names[] = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
static const char *midi_note_to_name(int note, char *buf, size_t size) {
if (note < 0 || note > 127) {
snprintf(buf, size, "???");
} else {
int octave = (note / 12) - 1;
const char *name = note_names[note % 12];
snprintf(buf, size, "%s%d", name, octave);
}
return buf;
}
static void terminate_old_notes(int current_port_idx, const snd_seq_event_t *original_on_ev, const snd_seq_event_t *processed_on_ev)
{
// If ticks_per_beat is not set, this feature is disabled to avoid division by zero.
if (app.ticks_per_beat <= 0) return;
unsigned int current_beat_tick = processed_on_ev->time.tick;
for (int ch = 0; ch < 16; ++ch) {
for (int nt = 0; nt < 128; ++nt) {
snd_seq_tick_time_t start_tick = app.active_note_start_tick[current_port_idx][ch][nt];
if (start_tick != (snd_seq_tick_time_t)-1) {
// Don't terminate notes started on the same tick, to allow chords.
if (current_beat_tick == start_tick) {
continue;
}
// Find the track for the note we are terminating
int track_idx = current_port_idx;
if (app.split_channels) {
track_idx = (track_idx * TRACKS_PER_PORT) + 1 + ch;
}
if (track_idx >= app.num_tracks) continue;
struct smf_track *track = &app.tracks[track_idx];
track->used = 1;
// Create a synthetic event just to pass the correct tick time
snd_seq_event_t off_ev;
snd_seq_ev_clear(&off_ev);
off_ev.time.tick = processed_on_ev->time.tick;
// Record the NOTEOFF event
delta_time(track, &off_ev);
command(track, 0x80 | ch);
add_byte(track, nt);
add_byte(track, 0); // Standard velocity for note-off
if (app.debug_mode) {
char note_name_buf[8];
midi_note_to_name(nt, note_name_buf, sizeof(note_name_buf));
double beat_pos = 0.0, offset_percent = 0.0;
long offset_ms = 0;
if (app.ticks_per_beat > 0 && app.bpm > 0 && app.queue_time_ref_set) {
beat_pos = (double)off_ev.time.tick / app.ticks_per_beat;
snd_seq_tick_time_t nearest_beat_tick = floor(beat_pos + 0.5) * app.ticks_per_beat;
if (snd_seq_ev_is_real(original_on_ev)) {
// Precise timing: Compare event real time to ideal grid real time
double tick_ns = (double)app.bpm * app.ticks_per_beat;
double ns_per_tick = (60.0 * 1000000000.0) / tick_ns;
long long ref_ns = (long long)app.ref_real_time.tv_sec * 1000000000LL + app.ref_real_time.tv_nsec;
long long ticks_from_ref_to_beat = (long long)nearest_beat_tick - (long long)app.ref_tick;
long long expected_beat_ns = ref_ns + (long long)(ticks_from_ref_to_beat * ns_per_tick);
long long actual_event_ns = (long long)original_on_ev->time.time.tv_sec * 1000000000LL + original_on_ev->time.time.tv_nsec;
long long ns_offset = actual_event_ns - expected_beat_ns;
if (ns_offset > tick_ns / 2 )
ns_offset = ns_offset - tick_ns;
offset_ms = ns_offset / 1000000LL;
double ns_per_beat = ns_per_tick * app.ticks_per_beat;
if (ns_per_beat > 0)
offset_percent = (double)ns_offset * 100.0 / ns_per_beat;
} else {
// Fallback to tick-based timing for events that came from the queue
long tick_offset = off_ev.time.tick - nearest_beat_tick;
double ms_per_tick = (60.0 * 1000.0) / ((double)app.bpm * app.ticks_per_beat);
offset_ms = round((double)tick_offset * ms_per_tick);
offset_percent = (double)tick_offset * 100.0 / app.ticks_per_beat;
}
}
if (offset_percent > 50) offset_percent = offset_percent - 100;
snd_seq_tick_time_t duration_ticks = off_ev.time.tick - start_tick;
double duration_beats = (app.ticks_per_beat > 0) ? (double)duration_ticks / app.ticks_per_beat : 0.0;
printf("DEBUG: OFF %-4s ch=%2d vel=%3d @ beat %.2f | timing: %+.2f%% (%+ldms) | duration: %.2f beats | auto-terminated by new note \n",
note_name_buf, ch, 0, beat_pos, offset_percent, offset_ms, duration_beats);
}
// Update state to mark note as off
app.active_note_start_tick[current_port_idx][ch][nt] = (snd_seq_tick_time_t)-1;
}
}
}
}
static void record_event(const snd_seq_event_t *ev) {
int port_idx, track_idx;
struct smf_track *track;
snd_seq_event_t event_with_tick; // A mutable copy of the event with a guaranteed tick timestamp
// Filter 1: Must be from our recording queue.
// We accept both tick and real-time events. The ALSA sequencer is expected
// to deliver a tick-stamped event from the queue, but in practice, it
// sometimes only delivers the initial real-time event.
if (ev->queue != app.queue_id) {
if (app.debug_mode && ev->type >= SND_SEQ_EVENT_NOTEON && ev->type <= SND_SEQ_EVENT_SYSEX) {
// printf(" -> REJECTED (Filter 1): Event from queue %d is not our recording queue %d.\n", ev->queue, app.queue_id);
}
return;
}
// Create a mutable copy of the event and normalize its timestamp to ticks.
event_with_tick = *ev;
if (snd_seq_ev_is_tick(ev)) {
// Event already has a tick timestamp, use it as-is.
// This is the case for metronome loopback and correctly scheduled MIDI events.
} else if (snd_seq_ev_is_real(ev)) {
// This is a "raw" real-time event. We convert its timestamp to a tick
// value by querying the current state of our recording queue.
snd_seq_queue_status_t *qstatus;
snd_seq_queue_status_alloca(&qstatus);
if (snd_seq_get_queue_status(app.seq, app.queue_id, qstatus) < 0) {
fprintf(stderr, "Warning: could not get queue status to timestamp a real-time event; event ignored.\n"); fflush(stdout);
return;
}
event_with_tick.time.tick = snd_seq_queue_status_get_tick_time(qstatus);
if (app.debug_mode) {
// printf(" -> INFO: Handled real-time event by converting to tick %u\n", event_with_tick.time.tick);
}
} else {
// Event has no timestamp; it cannot be recorded.
return;
}
// Filter 2: Must be destined for one of our recording input ports
port_idx = -1;
for (int i = 0; i < app.record_in_count; ++i) {
if (event_with_tick.dest.port == app.record_in_port_ids[i]) {
port_idx = i;
break;
}
}
if (port_idx == -1) {
if (app.debug_mode && event_with_tick.type >= SND_SEQ_EVENT_NOTEON && event_with_tick.type <= SND_SEQ_EVENT_SYSEX) {
// printf(" -> REJECTED (Filter 2): Destination port %d is not on the record list.\n", event_with_tick.dest.port);
}
return;
}
track_idx = port_idx;
if (app.split_channels) {
track_idx *= TRACKS_PER_PORT;
if (snd_seq_ev_is_channel_type(&event_with_tick))
track_idx += 1 + (event_with_tick.data.note.channel & 0xf);
}
if (track_idx >= app.num_tracks) return;
track = &app.tracks[track_idx];
track->used = 1;
// From here on, we use our normalized `event_with_tick` for all processing.
switch (event_with_tick.type) {
case SND_SEQ_EVENT_NOTEON:
if (event_with_tick.data.note.velocity == 0) goto handle_noteoff; // Running status note-off
if (app.on_switches_others_off) {
terminate_old_notes(port_idx, ev, &event_with_tick);
}
app.active_note_start_tick[port_idx][event_with_tick.data.note.channel & 0xf][event_with_tick.data.note.note & 0x7f] = event_with_tick.time.tick;
if (app.debug_mode) {
char note_name_buf[8];
midi_note_to_name(event_with_tick.data.note.note, note_name_buf, sizeof(note_name_buf));
double beat_pos = 0.0, offset_percent = 0.0;
long offset_ms = 0;
if (app.ticks_per_beat > 0 && app.bpm > 0 && app.queue_time_ref_set) {
beat_pos = (double)event_with_tick.time.tick / app.ticks_per_beat;
snd_seq_tick_time_t nearest_beat_tick = floor(beat_pos + 0.5) * app.ticks_per_beat;
if (snd_seq_ev_is_real(ev)) {
// Precise timing: Compare event real time to ideal grid real time
double ns_per_tick = (60.0 * 1000000000.0) / ((double)app.bpm * app.ticks_per_beat);
long long ref_ns = (long long)app.ref_real_time.tv_sec * 1000000000LL + app.ref_real_time.tv_nsec;
long long ticks_from_ref_to_beat = (long long)nearest_beat_tick - (long long)app.ref_tick;
long long expected_beat_ns = ref_ns + (long long)(ticks_from_ref_to_beat * ns_per_tick);
long long actual_event_ns = (long long)ev->time.time.tv_sec * 1000000000LL + ev->time.time.tv_nsec;
long long ns_offset = actual_event_ns - expected_beat_ns;
if (ns_offset > ns_per_tick / 2) ns_offset -= ns_per_tick;
offset_ms = ns_offset / 1000000LL;
offset_percent = (double)ns_offset * 100.0 / ns_per_tick;
} else {
// Fallback to tick-based timing for events that came from the queue
long tick_offset = event_with_tick.time.tick - nearest_beat_tick;
double ms_per_tick = (60.0 * 1000.0) / ((double)app.bpm * app.ticks_per_beat);
offset_ms = round((double)tick_offset * ms_per_tick);
if (offset_ms > ms_per_tick)
offset_ms -= ms_per_tick;
offset_percent = offset_ms / ms_per_tick;
}
}
printf("DEBUG: ON %-4s ch=%2d vel=%3d @ beat %.2f | timing: %+.2f%% (%+ldms)\n",
note_name_buf, event_with_tick.data.note.channel, event_with_tick.data.note.velocity,
beat_pos, offset_percent, offset_ms); fflush(stdout);
}
delta_time(track, &event_with_tick);
command(track, 0x90 | (event_with_tick.data.note.channel & 0xf));
add_byte(track, event_with_tick.data.note.note & 0x7f);
add_byte(track, event_with_tick.data.note.velocity & 0x7f);
break;
case SND_SEQ_EVENT_NOTEOFF:
handle_noteoff:
if (app.ignore_off)
return;
if (app.debug_mode) {
unsigned char channel = event_with_tick.data.note.channel & 0xf;
unsigned char note = event_with_tick.data.note.note & 0x7f;
snd_seq_tick_time_t start_tick = app.active_note_start_tick[port_idx][channel][note];
char note_name_buf[8];
midi_note_to_name(note, note_name_buf, sizeof(note_name_buf));
double beat_pos = 0.0, offset_percent = 0.0;
long offset_ms = 0;
if (app.ticks_per_beat > 0 && app.bpm > 0 && app.queue_time_ref_set) {
beat_pos = (double)event_with_tick.time.tick / app.ticks_per_beat;
snd_seq_tick_time_t nearest_beat_tick = floor(beat_pos + 0.5) * app.ticks_per_beat;
if (snd_seq_ev_is_real(ev)) {
// Precise timing: Compare event real time to ideal grid real time
double ns_per_tick = (60.0 * 1000000000.0) / ((double)app.bpm * app.ticks_per_beat);
long long ref_ns = (long long)app.ref_real_time.tv_sec * 1000000000LL + app.ref_real_time.tv_nsec;
long long ticks_from_ref_to_beat = (long long)nearest_beat_tick - (long long)app.ref_tick;
long long expected_beat_ns = ref_ns + (long long)(ticks_from_ref_to_beat * ns_per_tick);
long long actual_event_ns = (long long)ev->time.time.tv_sec * 1000000000LL + ev->time.time.tv_nsec;
long long ns_offset = actual_event_ns - expected_beat_ns;
offset_ms = ns_offset / 1000000LL;
double ns_per_beat = ns_per_tick * app.ticks_per_beat;
if (ns_per_beat > 0)
offset_percent = (double)ns_offset * 100.0 / ns_per_beat;
} else {
// Fallback to tick-based timing for events that came from the queue
long tick_offset = event_with_tick.time.tick - nearest_beat_tick;
double ms_per_tick = (60.0 * 1000.0) / ((double)app.bpm * app.ticks_per_beat);
offset_ms = round((double)tick_offset * ms_per_tick);
offset_percent = (double)tick_offset * 100.0 / app.ticks_per_beat;
}
}
snd_seq_tick_time_t duration_ticks = 0;
if (start_tick != (snd_seq_tick_time_t)-1 && event_with_tick.time.tick >= start_tick) {
duration_ticks = event_with_tick.time.tick - start_tick;
}
double duration_beats = (app.ticks_per_beat > 0) ? (double)duration_ticks / app.ticks_per_beat : 0.0;
printf("DEBUG: OFF %-4s ch=%2d vel=%3d @ beat %.2f | timing: %+.2f%% (%+ldms) | duration: %.2f beats\n",
note_name_buf, event_with_tick.data.note.channel, event_with_tick.data.note.velocity,
beat_pos, offset_percent, offset_ms, duration_beats); fflush(stdout);
}
app.active_note_start_tick[port_idx][event_with_tick.data.note.channel & 0xf][event_with_tick.data.note.note & 0x7f] = (snd_seq_tick_time_t)-1;
delta_time(track, &event_with_tick);
command(track, 0x80 | (event_with_tick.data.note.channel & 0xf));
add_byte(track, event_with_tick.data.note.note & 0x7f);
add_byte(track, event_with_tick.data.note.velocity & 0x7f);
break;
case SND_SEQ_EVENT_PGMCHANGE:
delta_time(track, &event_with_tick);
command(track, 0xc0 | (event_with_tick.data.control.channel & 0xf));
add_byte(track, event_with_tick.data.control.value & 0x7f);
break;
case SND_SEQ_EVENT_CONTROLLER:
delta_time(track, &event_with_tick);
command(track, 0xb0 | (event_with_tick.data.control.channel & 0xf));
add_byte(track, event_with_tick.data.control.param & 0x7f);
add_byte(track, event_with_tick.data.control.value & 0x7f);
break;
case SND_SEQ_EVENT_PITCHBEND:
delta_time(track, &event_with_tick);
command(track, 0xe0 | (event_with_tick.data.control.channel & 0xf));
add_byte(track, (event_with_tick.data.control.value + 8192) & 0x7f);
add_byte(track, ((event_with_tick.data.control.value + 8192) >> 7) & 0x7f);
break;
case SND_SEQ_EVENT_SYSEX:
delta_time(track, &event_with_tick);
add_byte(track, 0xf0);
track->last_command = 0;
var_value(track, event_with_tick.data.ext.len);
for (unsigned int i = 0; i < event_with_tick.data.ext.len; ++i)
add_byte(track, ((unsigned char*)event_with_tick.data.ext.ptr)[i]);
break;
default:
break;
}
}
/* --- Setup and Teardown --- */
static void setup_tracks() {
app.num_tracks = app.record_in_count;
if (app.split_channels) app.num_tracks *= TRACKS_PER_PORT;
app.tracks = calloc(app.num_tracks, sizeof(struct smf_track));
check_mem(app.tracks);
for (int i = 0; i < app.num_tracks; ++i)
app.tracks[i].cur_buf = &app.tracks[i].first_buf;
app.tracks[0].used = 1; // Track 0 always used for meta events
}
static void write_initial_meta_events() {
// Tempo
int usecs_per_quarter = 60000000 / app.bpm;
var_value(&app.tracks[0], 0);
add_byte(&app.tracks[0], 0xff); add_byte(&app.tracks[0], 0x51); add_byte(&app.tracks[0], 3);
add_byte(&app.tracks[0], usecs_per_quarter >> 16);
add_byte(&app.tracks[0], (usecs_per_quarter >> 8) & 0xff);
add_byte(&app.tracks[0], usecs_per_quarter & 0xff);
// Time Signature
var_value(&app.tracks[0], 0);
add_byte(&app.tracks[0], 0xff); add_byte(&app.tracks[0], 0x58); add_byte(&app.tracks[0], 4);
add_byte(&app.tracks[0], app.ts_num);
add_byte(&app.tracks[0], app.ts_den_pow2);
add_byte(&app.tracks[0], 24); // MIDI clocks per metronome click
add_byte(&app.tracks[0], 8); // 32nd notes per quarter note
}
static void terminate_all_active_notes(void)
{
if (!app.ignore_off)
return;
snd_seq_queue_status_t *qstatus;
snd_seq_queue_status_alloca(&qstatus);
check_snd(snd_seq_get_queue_status(app.seq, app.queue_id, qstatus), "get queue status");
snd_seq_tick_time_t final_tick = snd_seq_queue_status_get_tick_time(qstatus);
// printf("Terminating any remaining notes at tick %u...\n", final_tick);
for (int p_idx = 0; p_idx < app.record_in_count; ++p_idx) {
for (int ch = 0; ch < 16; ++ch) {
for (int nt = 0; nt < 128; ++nt) {
snd_seq_tick_time_t start_tick = app.active_note_start_tick[p_idx][ch][nt];
if (start_tick != (snd_seq_tick_time_t)-1) {
int track_idx = p_idx;
if (app.split_channels) {
track_idx = (p_idx * TRACKS_PER_PORT) + 1 + ch;
}
if (track_idx >= app.num_tracks) continue;
struct smf_track *track = &app.tracks[track_idx];
track->used = 1; // Mark track as used if it wasn't already
snd_seq_event_t off_ev;
snd_seq_ev_clear(&off_ev);
off_ev.time.tick = final_tick;
delta_time(track, &off_ev);
command(track, 0x80 | ch);
add_byte(track, nt);
add_byte(track, 0);
if (app.debug_mode) {
char note_name_buf[8];
midi_note_to_name(nt, note_name_buf, sizeof(note_name_buf));
double beat_pos = 0.0, offset_percent = 0.0;
long offset_ms = 0;
if (app.ticks_per_beat > 0 && app.bpm > 0 && app.queue_time_ref_set) {
beat_pos = (double)off_ev.time.tick / app.ticks_per_beat;
// NOTE: Cannot calculate real-time offset as we don't have a real event timestamp.
// We fall back to calculating offset from the nearest beat based on ticks alone.
long nearest_beat_tick = floor(beat_pos + 0.5) * app.ticks_per_beat;
long tick_offset = off_ev.time.tick - nearest_beat_tick;
double ms_per_tick = (60.0 * 1000.0) / ((double)app.bpm * app.ticks_per_beat);
offset_ms = round((double)tick_offset * ms_per_tick);
if (offset_ms > ms_per_tick/ 2) offset_ms -= ms_per_tick;
offset_percent = offset_ms / ms_per_tick;
}
snd_seq_tick_time_t duration_ticks = off_ev.time.tick - start_tick;
double duration_beats = (app.ticks_per_beat > 0) ? (double)duration_ticks / app.ticks_per_beat : 0.0;
printf("DEBUG: OFF %-4s ch=%2d vel=%3d @ beat %.2f | auto-terminated at exit | timing: %+.2f%% (%+ldms) | duration: %.2f beats\n",
note_name_buf, ch, 0, beat_pos, offset_percent, offset_ms, duration_beats); fflush(stdout);
}
}
}
}
}
}
static void finish_tracks() {
snd_seq_queue_status_t *qstatus;
snd_seq_queue_status_alloca(&qstatus);
check_snd(snd_seq_get_queue_status(app.seq, app.queue_id, qstatus), "get queue status");
snd_seq_tick_time_t tick = snd_seq_queue_status_get_tick_time(qstatus);
for (int i = 0; i < app.num_tracks; ++i) {
if (!app.tracks[i].used) continue;
int diff = tick - app.tracks[i].last_tick;
if (diff < 0) diff = 0;
var_value(&app.tracks[i], diff);
add_byte(&app.tracks[i], 0xff); add_byte(&app.tracks[i], 0x2f); add_byte(&app.tracks[i], 0);
}
}
static void write_smf_file() {
int used_tracks = 0, i;
for (i = 0; i < app.num_tracks; ++i) if (app.tracks[i].used) used_tracks++;
if (used_tracks == 0) {
printf("No events recorded, output file will not be created.\n");
return;
}
app.file = fopen(app.output_filename, "wb");
if (!app.file) fatal("Cannot open %s for writing - %s", app.output_filename, strerror(errno));
// MThd chunk
fwrite("MThd\0\0\0\6", 1, 8, app.file);
fputc(0, app.file); fputc(used_tracks > 1 ? 1 : 0, app.file);
fputc((used_tracks >> 8) & 0xff, app.file); fputc(used_tracks & 0xff, app.file);
fputc(app.ticks_per_beat >> 8, app.file); fputc(app.ticks_per_beat & 0xff, app.file);
// MTrk chunks
for (i = 0; i < app.num_tracks; ++i) {
struct smf_track *track = &app.tracks[i];
if (!track->used) continue;
fwrite("MTrk", 1, 4, app.file);
fputc((track->size >> 24) & 0xff, app.file);
fputc((track->size >> 16) & 0xff, app.file);
fputc((track->size >> 8) & 0xff, app.file);
fputc(track->size & 0xff, app.file);
struct buffer *buf;
for (buf = &track->first_buf; buf; buf = buf->next)
fwrite(buf->buf, 1, (buf == track->cur_buf) ? track->cur_buf_size : BUFFER_SIZE, app.file);
}
fclose(app.file);
}
static void setup_alsa() {
snd_seq_port_info_t *pinfo;
snd_seq_addr_t addr;
check_snd(snd_seq_open(&app.seq, "default", SND_SEQ_OPEN_DUPLEX, 0), "open sequencer");
app.client_id = snd_seq_client_id(app.seq);
check_snd(app.client_id, "get client id");
check_snd(snd_seq_set_client_name(app.seq, "arecordmidi"), "set client name");
app.queue_id = snd_seq_alloc_named_queue(app.seq, "arecordmidi queue");
check_snd(app.queue_id, "create queue");
snd_seq_port_info_alloca(&pinfo);
for (int i = 0; i < app.record_in_count; ++i) {
char port_name[64];
snprintf(port_name, sizeof(port_name), "Record In %d", i + 1);
app.record_in_port_ids[i] = snd_seq_create_simple_port(app.seq, port_name,
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_APPLICATION);
check_snd(app.record_in_port_ids[i], "create record port");
// Configure the port for timestamping events and scheduling them on our queue
check_snd(snd_seq_get_port_info(app.seq, app.record_in_port_ids[i], pinfo), "get port info");
snd_seq_port_info_set_timestamping(pinfo, 1);
snd_seq_port_info_set_timestamp_real(pinfo, 1);
snd_seq_port_info_set_timestamp_queue(pinfo, app.queue_id);
check_snd(snd_seq_set_port_info(app.seq, app.record_in_port_ids[i], pinfo), "configure record port");
// Connect the external source to our new port using an explicit subscription
// that specifies the queue. This is more robust than relying on the port's
// properties alone.
snd_seq_port_subscribe_t *subs;
snd_seq_port_subscribe_alloca(&subs);
check_snd(snd_seq_parse_address(app.seq, &addr, app.record_in_addrs[i]), app.record_in_addrs[i]);
snd_seq_addr_t my_dest_addr;
my_dest_addr.client = app.client_id;
my_dest_addr.port = app.record_in_port_ids[i];
snd_seq_port_subscribe_set_sender(subs, &addr);
snd_seq_port_subscribe_set_dest(subs, &my_dest_addr);
snd_seq_port_subscribe_set_queue(subs, app.queue_id);
check_snd(snd_seq_subscribe_port(app.seq, subs), "connect record port");
}
if (app.use_metronome) {
app.metronome_out_port_id = snd_seq_create_simple_port(app.seq, "Metronome Out",
SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ, SND_SEQ_PORT_TYPE_APPLICATION);
check_snd(app.metronome_out_port_id, "create metronome out port");
check_snd(snd_seq_parse_address(app.seq, &addr, app.metronome_out_addr), app.metronome_out_addr);
check_snd(snd_seq_connect_to(app.seq, app.metronome_out_port_id, addr.client, addr.port), "connect metronome port");
app.metronome_loop_port_id = snd_seq_create_simple_port(app.seq, "Metronome Loop",
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_APPLICATION);
check_snd(app.metronome_loop_port_id, "create metronome loop port");
check_snd(snd_seq_connect_from(app.seq, app.metronome_loop_port_id, app.client_id, app.metronome_loop_port_id), "connect metronome loopback");
}
if (app.mode == MODE_MASTER) {
snd_seq_queue_tempo_t *tempo;
snd_seq_queue_tempo_alloca(&tempo);
snd_seq_queue_tempo_set_tempo(tempo, 60000000 / app.bpm);
snd_seq_queue_tempo_set_ppq(tempo, app.ticks_per_beat);
check_snd(snd_seq_set_queue_tempo(app.seq, app.queue_id, tempo), "set queue tempo");
app.clock_out_port_id = snd_seq_create_simple_port(app.seq, "Clock Out",
SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ, SND_SEQ_PORT_TYPE_MIDI_GENERIC);
check_snd(app.clock_out_port_id, "create clock out port");
snd_seq_addr_t sender = { .client = SND_SEQ_CLIENT_SYSTEM, .port = SND_SEQ_PORT_SYSTEM_TIMER };
snd_seq_addr_t dest = { .client = app.client_id, .port = app.clock_out_port_id };
snd_seq_port_subscribe_t *subs;
snd_seq_port_subscribe_alloca(&subs);
snd_seq_port_subscribe_set_sender(subs, &sender);
snd_seq_port_subscribe_set_dest(subs, &dest);
snd_seq_port_subscribe_set_queue(subs, app.queue_id);
snd_seq_port_subscribe_set_time_update(subs, 1);
check_snd(snd_seq_subscribe_port(app.seq, subs), "subscribe clock out to system timer");
if (app.clock_out_addr) {
check_snd(snd_seq_parse_address(app.seq, &addr, app.clock_out_addr), app.clock_out_addr);
check_snd(snd_seq_connect_to(app.seq, app.clock_out_port_id, addr.client, addr.port), "connect clock out port");
}
} else { // MODE_SLAVE
snd_seq_queue_timer_t *timer;
snd_seq_queue_timer_alloca(&timer);
snd_seq_queue_timer_set_type(timer, SND_SEQ_TIMER_ALSA);
snd_seq_queue_timer_set_id(timer, 0);
check_snd(snd_seq_set_queue_timer(app.seq, app.queue_id, timer), "set queue timer");
snd_seq_addr_t sender, dest;
check_snd(snd_seq_parse_address(app.seq, &sender, app.clock_in_addr), app.clock_in_addr);
dest.client = SND_SEQ_CLIENT_SYSTEM;
dest.port = SND_SEQ_PORT_SYSTEM_TIMER;
snd_seq_port_subscribe_t *subs;
snd_seq_port_subscribe_alloca(&subs);
snd_seq_port_subscribe_set_sender(subs, &sender);
snd_seq_port_subscribe_set_dest(subs, &dest);
check_snd(snd_seq_subscribe_port(app.seq, subs), "subscribe system timer to external clock");
}
}
static void list_ports_and_exit() {
snd_seq_client_info_t *cinfo;
snd_seq_port_info_t *pinfo;
snd_seq_t *seq;
check_snd(snd_seq_open(&seq, "default", SND_SEQ_OPEN_INPUT, 0), "open sequencer for listing");
snd_seq_client_info_alloca(&cinfo);
snd_seq_port_info_alloca(&pinfo);
puts(" Port Client name Port name");
puts("-----------------------------------------------------------------");
snd_seq_client_info_set_client(cinfo, -1);
while (snd_seq_query_next_client(seq, cinfo) >= 0) {
int client = snd_seq_client_info_get_client(cinfo);
snd_seq_port_info_set_client(pinfo, client);
snd_seq_port_info_set_port(pinfo, -1);
while (snd_seq_query_next_port(seq, pinfo) >= 0) {
unsigned int caps = snd_seq_port_info_get_capability(pinfo);
if ((caps & SND_SEQ_PORT_CAP_SUBS_READ) || (caps & SND_SEQ_PORT_CAP_READ)) {
printf("%3d:%-3d %-32.32s %s\n",
snd_seq_port_info_get_client(pinfo),
snd_seq_port_info_get_port(pinfo),
snd_seq_client_info_get_name(cinfo),
snd_seq_port_info_get_name(pinfo)); fflush(stdout);
}
}
}
snd_seq_close(seq);
exit(0);
}
/* --- Main Logic --- */
static void sighandler(int signal) {
(void)signal;
app.stop_request = 1;
}
static void help(const char *argv0) {
fprintf(stderr, "Usage: %s [options] outputfile.mid\n"
"\nMODES (choose one, --master is default):\n"
" --master Provide MIDI clock and be the master (default)\n"
" --slave Receive MIDI clock from another device\n"
"\nPORT CONFIGURATION:\n"
" -p, --midi-in-record=c:p Add a source port to record from (can be used multiple times)\n"
" -m, --midi-out-metronome=c:p Play metronome to this destination port\n"
" --midi-in-clock=c:p In slave mode, receive clock from here\n"
" --midi-out-clock=c:p In master mode, send MIDI clock to here (optional)\n"
"\nTIMING & RECORDING:\n"
" -b, --bpm=beats Tempo in beats per minute (master mode)\n"
" -t, --ticks=ticks Resolution in ticks per beat (PPQ)\n"
" -i, --timesig=nn:dd Time signature for metronome (e.g., 4:4, 3:4)\n"
" -s, --split-channels Create a separate track for each MIDI channel\n"
" --note-recording-on-switches-others-off\n"
" A new note-on event terminates other sounding notes\n"
" --note-recording-ignore-off\n"
" Ignore note-off events (implies the above option)\n"
"\nMETRONOME TWEAKS:\n"
" --compensate-metronome-output-latency-ms=<ms> Send metronome notes early\n"
"\nOTHER OPTIONS:\n"
" -h, --help This help\n"
" -V, --version Show version\n"
" -l, --list List available MIDI ports\n"
" --debug Output recorded events to the console\n",
argv0);
}
static void parse_time_signature(const char *arg) {
char *sep;
app.ts_num = strtol(arg, &sep, 10);
if (app.ts_num < 1 || app.ts_num > 64 || (*sep != ':' && *sep != '/'))
fatal("Invalid time signature numerator: %s", arg);
app.ts_den = strtol(++sep, NULL, 10);
if (app.ts_den < 1 || (app.ts_den & (app.ts_den - 1)) != 0) // Must be power of 2
fatal("Invalid time signature denominator (must be power of 2): %s", arg);
app.ts_den_pow2 = 0;
for (int x = app.ts_den; x > 1; x /= 2)
++app.ts_den_pow2;
}
static void validate_args() {
if (app.mode == MODE_UNSPECIFIED) app.mode = MODE_MASTER;
if (app.record_in_count == 0) fatal("Please specify at least one source port with -p or --midi-in-record.");
if (app.mode == MODE_SLAVE && !app.clock_in_addr) fatal("Slave mode requires --midi-in-clock.");
if (app.mode == MODE_MASTER && app.clock_in_addr) fatal("--midi-in-clock can only be used in slave mode.");
if (app.mode == MODE_SLAVE && app.clock_out_addr) fatal("--midi-out-clock can only be used in master mode.");
if (app.use_metronome && !app.metronome_out_addr) fatal("Using metronome requires specifying --midi-out-metronome.");
if (app.metro_latency_ms > 0) {
double ticks_per_ms = (app.bpm / 60.0 * app.ticks_per_beat) / 1000.0;
app.metro_tick_offset = (snd_seq_tick_time_t)(app.metro_latency_ms * ticks_per_ms);
}
if (app.ignore_off) {
app.on_switches_others_off = 1;
}
}
int main(int argc, char *argv[]) {
app.mode = MODE_UNSPECIFIED;
app.bpm = 120; app.ticks_per_beat = 384;
app.ts_num = 4; app.ts_den = 4; app.ts_den_pow2 = 2; // Default 4/4
app.metro_channel = 9; app.metro_strong_note = 34; app.metro_weak_note = 33; app.metro_velocity = 100;
app.queue_time_ref_set = 0;
static const struct option long_options[] = {
{"help", 0, NULL, 'h'}, {"version", 0, NULL, 'V'}, {"list", 0, NULL, 'l'},
{"master", 0, NULL, 1000}, {"slave", 0, NULL, 1001},
{"midi-in-record", 1, NULL, 'p'}, {"midi-out-metronome", 1, NULL, 'm'},
{"midi-in-clock", 1, NULL, 1002}, {"midi-out-clock", 1, NULL, 1003},
{"bpm", 1, NULL, 'b'}, {"ticks", 1, NULL, 't'}, {"timesig", 1, NULL, 'i'},
{"split-channels", 0, NULL, 's'},
{"debug", 0, NULL, 1004},
{"compensate-metronome-output-latency-ms", 1, NULL, 1005},
{"note-recording-on-switches-others-off", 0, NULL, 1006},
{"note-recording-ignore-off", 0, NULL, 1007},
{0}
};
int c;
while ((c = getopt_long(argc, argv, "hVlp:m:b:t:i:s", long_options, NULL)) != -1) {
switch (c) {
case 'h': help(argv[0]); return 0;
case 'V': fputs("arecordmidi version " SND_UTIL_VERSION_STR "\n", stderr); return 0;
case 'l': list_ports_and_exit(); break;
case 1000: if (app.mode != MODE_UNSPECIFIED) fatal("--master and --slave are mutually exclusive."); app.mode = MODE_MASTER; break;
case 1001: if (app.mode != MODE_UNSPECIFIED) fatal("--master and --slave are mutually exclusive."); app.mode = MODE_SLAVE; break;
case 'p': if (app.record_in_count >= MAX_PORTS) fatal("Too many --midi-in-record ports (max %d)", MAX_PORTS); app.record_in_addrs[app.record_in_count++] = optarg; break;
case 'm': app.metronome_out_addr = optarg; app.use_metronome = 1; break;
case 1002: app.clock_in_addr = optarg; break;
case 1003: app.clock_out_addr = optarg; break;
case 'b': app.bpm = atoi(optarg); break;
case 't': app.ticks_per_beat = atoi(optarg); break;
case 'i': parse_time_signature(optarg); break;
case 's': app.split_channels = 1; break;
case 1004: app.debug_mode = 1; break;
case 1005: app.metro_latency_ms = atol(optarg); break;
case 1006: app.on_switches_others_off = 1; break;
case 1007: app.ignore_off = 1; break;
default: help(argv[0]); return 1;
}
}
if (optind >= argc) fatal("Please specify an output file name.");
app.output_filename = argv[optind];
validate_args();
// Initialize the active note tracker for debug output
memset(app.active_note_start_tick, -1, sizeof(app.active_note_start_tick));
setup_alsa();
setup_tracks();
write_initial_meta_events();
if (app.mode == MODE_MASTER) {
printf("Master mode: Starting recording at %d BPM.\n", app.bpm);
check_snd(snd_seq_start_queue(app.seq, app.queue_id, NULL), "start queue");
// For debug timing, get a reliable reference time from ALSA itself
// immediately after starting the queue. This avoids clock source mismatches.
if (app.debug_mode) {
snd_seq_queue_status_t *qstatus;
snd_seq_queue_status_alloca(&qstatus);
check_snd(snd_seq_get_queue_status(app.seq, app.queue_id, qstatus), "get queue status for time ref");
app.ref_tick = snd_seq_queue_status_get_tick_time(qstatus);
app.ref_real_time = *snd_seq_queue_status_get_real_time(qstatus);
app.queue_time_ref_set = 1;
}
start_metronome();
} else {
printf("Slave mode: Waiting for MIDI clock from %s...\n", app.clock_in_addr);
snd_seq_control_queue(app.seq, app.queue_id, SND_SEQ_EVENT_START, 0, NULL);
}
signal(SIGINT, sighandler);
signal(SIGTERM, sighandler);
printf("Recording to '%s'. Press Ctrl+C to stop.\n", app.output_filename);
int npfds = snd_seq_poll_descriptors_count(app.seq, POLLIN);
struct pollfd *pfds = alloca(sizeof(*pfds) * npfds);
while (!app.stop_request) {
snd_seq_poll_descriptors(app.seq, pfds, npfds, POLLIN);
if (poll(pfds, npfds, 1000) < 0 && errno != EINTR) break;
snd_seq_event_t *ev;
while (snd_seq_event_input(app.seq, &ev) > 0 && !app.stop_request) {
if (app.debug_mode) {
// printf("INPUT EVENT: type=%-3d src=%-3d:%-3d dest=%-3d:%-3d queue=%-3d ",
// ev->type,
// ev->source.client, ev->source.port,
// ev->dest.client, ev->dest.port,
// ev->queue);
if (snd_seq_ev_is_tick(ev)) {
// printf("time=tick:%u\n", ev->time.tick);
} else if (snd_seq_ev_is_real(ev)) {
// printf("time=real:%ld.%09ld\n", (long)ev->time.time.tv_sec, (long)ev->time.time.tv_nsec);
} else {
// printf("time=none\n");
}
}
// Handle slave mode start, which starts the queue
if (ev->type == SND_SEQ_EVENT_START && app.mode == MODE_SLAVE) {
printf("Received MIDI Start. Starting recording and metronome.\n");
if (app.debug_mode && !app.queue_time_ref_set) {
app.ref_tick = ev->time.tick; // should be 0
app.ref_real_time = ev->time.time;
app.queue_time_ref_set = 1;
}
start_metronome();
}
// Handle metronome loopback event
if (app.use_metronome && ev->dest.port == app.metronome_loop_port_id && ev->type == SND_SEQ_EVENT_USR0) {
metronome_pattern(ev->time.tick);
}
// Attempt to record any other event. The function itself will filter.
record_event(ev);
snd_seq_free_event(ev);
}
}
printf("\nRecording stopped. Finalizing file...\n");
terminate_all_active_notes();
finish_tracks();
write_smf_file();
for (int i = 0; i < app.num_tracks; ++i) {
struct buffer *b = app.tracks[i].first_buf.next, *next;
while (b) {
next = b->next;
free(b);
b = next;
}
}
free(app.tracks);
snd_seq_close(app.seq);
printf("Done.\n");
return 0;
}
```
Issue URL : https://github.com/alsa-project/alsa-utils/issues/307
Repository URL: https://github.com/alsa-project/alsa-utils
1
0
ucm2: sof: hdmi: Add 5.1 and 7.1 variants of HDMI devices when using IPC4
by GitHub pull_request - edited 31 Oct '25
by GitHub pull_request - edited 31 Oct '25
31 Oct '25
alsa-project/alsa-ucm-conf pull request #633 was edited from ujfalusi:
Systems using IPC4 can support up to 8 channels of audio (and passthrough) via HDMI.
In UCM the default PlaybackChannels is set to 2, which prevents users from selecting multichannel configurations.
When probing the card, Pipewire will drop configurations that are not supported either by the PCM device or based on ELD information.
This means that if the equipment supports only stereo then the 5.1 and 7.1 variants should not be visible, if the equipment is 5.1 capable, then only the 7.1 variant is removed.
The kernel will refine the PCM parameters based on the ELD information as wall when https://lore.kernel.org/linux-sound/20251029073600.13624-1-peter.ujfalusi@l… is applied.
@perexg, @wtay, @ford-prefect, is this something which can help PW and user space to handle the HDMI a bit better with SOF?
Request URL : https://github.com/alsa-project/alsa-ucm-conf/pull/633
Patch URL : https://github.com/alsa-project/alsa-ucm-conf/pull/633.patch
Repository URL: https://github.com/alsa-project/alsa-ucm-conf
1
0
ucm2: sof: hdmi: Add 5.1 and 7.1 varriants of HDMI devices when using IPC4
by GitHub pull_request - opened 31 Oct '25
by GitHub pull_request - opened 31 Oct '25
31 Oct '25
alsa-project/alsa-ucm-conf pull request #633 was opened from ujfalusi:
Systems using IPC4 can support up to 8 channels of audio (and passthrough) via HDMI.
In UCM the default PlaybackChannels is set to 2, which prevents users from selecting multichannel configurations.
When probing the card, Pipewire will drop configurations that are not supported either by the PCM device or based on ELD information.
This means that if the equipment supports only stereo then the 5.1 and 7.1 variants should not be visible, if the equipment is 5.1 capable, then only the 7.1 variant is removed.
The kernel will refine the PCM parameters based on the ELD information as wall when https://lore.kernel.org/linux-sound/20251029073600.13624-1-peter.ujfalusi@l… is applied.
@perexg, @wtay, @ford-prefect, is this something which can help PW and user space to handle the HDMI a bit better with SOF?
Request URL : https://github.com/alsa-project/alsa-ucm-conf/pull/633
Patch URL : https://github.com/alsa-project/alsa-ucm-conf/pull/633.patch
Repository URL: https://github.com/alsa-project/alsa-ucm-conf
1
0
31 Oct '25
The ALC5575 integrates an audio DSP that typically loads its firmware
from an external flash via its own SPI host interface. In certain
hardware configurations, the firmware can alternatively be loaded
through the SPI client interface. The driver provides basic mute and
volume control functions. When the SPI client interface is enabled,
firmware loading is handled by the SPI driver.
Signed-off-by: Oder Chiou <oder_chiou(a)realtek.com>
---
sound/soc/codecs/Kconfig | 10 +
sound/soc/codecs/Makefile | 4 +
sound/soc/codecs/rt5575-spi.c | 95 +++++++++
sound/soc/codecs/rt5575-spi.h | 16 ++
sound/soc/codecs/rt5575.c | 366 ++++++++++++++++++++++++++++++++++
sound/soc/codecs/rt5575.h | 54 +++++
6 files changed, 545 insertions(+)
create mode 100644 sound/soc/codecs/rt5575-spi.c
create mode 100644 sound/soc/codecs/rt5575-spi.h
create mode 100644 sound/soc/codecs/rt5575.c
create mode 100644 sound/soc/codecs/rt5575.h
diff --git a/sound/soc/codecs/Kconfig b/sound/soc/codecs/Kconfig
index a0dfef57200c..cb4e14013cc8 100644
--- a/sound/soc/codecs/Kconfig
+++ b/sound/soc/codecs/Kconfig
@@ -211,6 +211,7 @@ config SND_SOC_ALL_CODECS
imply SND_SOC_RT1305
imply SND_SOC_RT1308
imply SND_SOC_RT5514
+ imply SND_SOC_RT5575
imply SND_SOC_RT5616
imply SND_SOC_RT5631
imply SND_SOC_RT5640
@@ -1767,6 +1768,15 @@ config SND_SOC_RT5514_SPI_BUILTIN
bool # force RT5514_SPI to be built-in to avoid link errors
default SND_SOC_RT5514=y && SND_SOC_RT5514_SPI=m
+config SND_SOC_RT5575
+ tristate
+ depends on I2C
+
+config SND_SOC_RT5575_SPI
+ tristate
+ depends on SPI_MASTER
+ select SND_SOC_RT5575
+
config SND_SOC_RT5616
tristate "Realtek RT5616 CODEC"
depends on I2C
diff --git a/sound/soc/codecs/Makefile b/sound/soc/codecs/Makefile
index 39138d96a720..82f660cbe8ec 100644
--- a/sound/soc/codecs/Makefile
+++ b/sound/soc/codecs/Makefile
@@ -252,6 +252,8 @@ snd-soc-rt286-y := rt286.o
snd-soc-rt298-y := rt298.o
snd-soc-rt5514-y := rt5514.o
snd-soc-rt5514-spi-y := rt5514-spi.o
+snd-soc-rt5575-y := rt5575.o
+snd-soc-rt5575-spi-y := rt5575-spi.o
snd-soc-rt5616-y := rt5616.o
snd-soc-rt5631-y := rt5631.o
snd-soc-rt5640-y := rt5640.o
@@ -684,6 +686,8 @@ obj-$(CONFIG_SND_SOC_RT298) += snd-soc-rt298.o
obj-$(CONFIG_SND_SOC_RT5514) += snd-soc-rt5514.o
obj-$(CONFIG_SND_SOC_RT5514_SPI) += snd-soc-rt5514-spi.o
obj-$(CONFIG_SND_SOC_RT5514_SPI_BUILTIN) += snd-soc-rt5514-spi.o
+obj-$(CONFIG_SND_SOC_RT5575) += snd-soc-rt5575.o
+obj-$(CONFIG_SND_SOC_RT5575_SPI) += snd-soc-rt5575-spi.o
obj-$(CONFIG_SND_SOC_RT5616) += snd-soc-rt5616.o
obj-$(CONFIG_SND_SOC_RT5631) += snd-soc-rt5631.o
obj-$(CONFIG_SND_SOC_RT5640) += snd-soc-rt5640.o
diff --git a/sound/soc/codecs/rt5575-spi.c b/sound/soc/codecs/rt5575-spi.c
new file mode 100644
index 000000000000..cf30d22e8a8f
--- /dev/null
+++ b/sound/soc/codecs/rt5575-spi.c
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * rt5575-spi.c -- ALC5575 SPI driver
+ *
+ * Copyright(c) 2025 Realtek Semiconductor Corp.
+ *
+ */
+
+#include <linux/of.h>
+#include <linux/spi/spi.h>
+
+#include "rt5575-spi.h"
+
+#define RT5575_SPI_BUF_LEN 240
+
+/* SPI Command */
+enum {
+ RT5575_SPI_CMD_16_READ = 0,
+ RT5575_SPI_CMD_16_WRITE,
+ RT5575_SPI_CMD_32_READ,
+ RT5575_SPI_CMD_32_WRITE,
+ RT5575_SPI_CMD_BURST_READ,
+ RT5575_SPI_CMD_BURST_WRITE,
+};
+
+struct rt5575_spi_burst_write {
+ u8 cmd;
+ u32 addr;
+ u8 data[RT5575_SPI_BUF_LEN];
+ u8 dummy;
+} __packed;
+
+bool rt5575_spi_ready;
+static struct spi_device *rt5575_spi;
+
+/**
+ * rt5575_spi_burst_write - Write data to SPI by rt5575 address.
+ * @addr: Start address.
+ * @txbuf: Data buffer for writng.
+ * @len: Data length.
+ *
+ */
+int rt5575_spi_burst_write(u32 addr, const u8 *txbuf, size_t len)
+{
+ struct rt5575_spi_burst_write buf = {
+ .cmd = RT5575_SPI_CMD_BURST_WRITE
+ };
+ unsigned int end, offset = 0;
+
+ while (offset < len) {
+ if (offset + RT5575_SPI_BUF_LEN <= len)
+ end = RT5575_SPI_BUF_LEN;
+ else
+ end = len % RT5575_SPI_BUF_LEN;
+
+ buf.addr = cpu_to_le32(addr + offset);
+
+ memcpy(&buf.data, &txbuf[offset], end);
+
+ spi_write(rt5575_spi, &buf, sizeof(struct rt5575_spi_burst_write));
+
+ offset += RT5575_SPI_BUF_LEN;
+ }
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(rt5575_spi_burst_write);
+
+static int rt5575_spi_probe(struct spi_device *spi)
+{
+ rt5575_spi = spi;
+
+ rt5575_spi_ready = true;
+
+ return 0;
+}
+
+static const struct of_device_id rt5575_of_match[] = {
+ { .compatible = "realtek,rt5575" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, rt5575_of_match);
+
+static struct spi_driver rt5575_spi_driver = {
+ .driver = {
+ .name = "rt5575",
+ .of_match_table = of_match_ptr(rt5575_of_match),
+ },
+ .probe = rt5575_spi_probe,
+};
+module_spi_driver(rt5575_spi_driver);
+
+MODULE_DESCRIPTION("ALC5575 SPI driver");
+MODULE_AUTHOR("Oder Chiou <oder_chiou(a)realtek.com>");
+MODULE_LICENSE("GPL");
diff --git a/sound/soc/codecs/rt5575-spi.h b/sound/soc/codecs/rt5575-spi.h
new file mode 100644
index 000000000000..cafe49a7ecc2
--- /dev/null
+++ b/sound/soc/codecs/rt5575-spi.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * rt5575-spi.h -- ALC5575 SPI driver
+ *
+ * Copyright(c) 2025 Realtek Semiconductor Corp.
+ *
+ */
+
+#ifndef __RT5575_SPI_H__
+#define __RT5575_SPI_H__
+
+extern bool rt5575_spi_ready;
+
+int rt5575_spi_burst_write(u32 addr, const u8 *txbuf, size_t len);
+
+#endif /* __RT5575_SPI_H__ */
diff --git a/sound/soc/codecs/rt5575.c b/sound/soc/codecs/rt5575.c
new file mode 100644
index 000000000000..58cceaac0705
--- /dev/null
+++ b/sound/soc/codecs/rt5575.c
@@ -0,0 +1,366 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * rt5575.c -- ALC5575 ALSA SoC audio component driver
+ *
+ * Copyright(c) 2025 Realtek Semiconductor Corp.
+ *
+ */
+
+#include <linux/firmware.h>
+#include <linux/i2c.h>
+#include <sound/soc.h>
+#include <sound/tlv.h>
+
+#include "rt5575.h"
+#if IS_ENABLED(CONFIG_SND_SOC_RT5575_SPI)
+#include "rt5575-spi.h"
+#endif
+
+static bool rt5575_readable_register(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case RT5575_ID:
+ case RT5575_ID_1:
+ case RT5575_MIXL_VOL:
+ case RT5575_MIXR_VOL:
+ case RT5575_PROMPT_VOL:
+ case RT5575_SPK01_VOL:
+ case RT5575_SPK23_VOL:
+ case RT5575_MIC1_VOL:
+ case RT5575_MIC2_VOL:
+ case RT5575_WNC_CTRL:
+ case RT5575_MODE_CTRL:
+ case RT5575_I2S_RATE_CTRL:
+ case RT5575_SLEEP_CTRL:
+ case RT5575_ALG_BYPASS_CTRL:
+ case RT5575_PINMUX_CTRL_2:
+ case RT5575_GPIO_CTRL_1:
+ case RT5575_DSP_BUS_CTRL:
+ case RT5575_SW_INT:
+ case RT5575_DSP_BOOT_ERR:
+ case RT5575_DSP_READY:
+ case RT5575_DSP_CMD_ADDR:
+ case RT5575_EFUSE_DATA_2:
+ case RT5575_EFUSE_DATA_3:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static const DECLARE_TLV_DB_SCALE(ob_tlv, -9525, 75, 0);
+
+#if IS_ENABLED(CONFIG_SND_SOC_RT5575_SPI)
+static void rt5575_spi_fw_loaded(const struct firmware *fw, void *context)
+{
+ struct rt5575_priv *rt5575 = context;
+ struct i2c_client *i2c = rt5575->i2c;
+ const struct firmware *firmware;
+ static const char * const fw_path[] = {
+ "realtek/rt5575/rt5575_fw2.bin", "realtek/rt5575/rt5575_fw3.bin",
+ "realtek/rt5575/rt5575_fw4.bin"
+ };
+ static const u32 fw_addr[] = { 0x5f600000, 0x5f7fe000, 0x5f7ff000 };
+ int i, ret;
+
+ regmap_write(rt5575->dsp_regmap, 0xfafafafa, 0x00000004);
+ regmap_write(rt5575->dsp_regmap, 0x18008064, 0x00000000);
+ regmap_write(rt5575->dsp_regmap, 0x18008068, 0x0002ffff);
+
+ rt5575_spi_burst_write(0x5f400000, fw->data, fw->size);
+ release_firmware(fw);
+
+ for (i = 0; i < ARRAY_SIZE(fw_addr); i++) {
+ ret = request_firmware(&firmware, fw_path[i], &i2c->dev);
+ if (ret == 0) {
+ rt5575_spi_burst_write(fw_addr[i], firmware->data, firmware->size);
+ release_firmware(firmware);
+ }
+ }
+
+ regmap_write(rt5575->dsp_regmap, 0x18000000, 0x00000000);
+
+ regmap_update_bits(rt5575->regmap, RT5575_SW_INT, 1, 1);
+
+ regmap_read_poll_timeout(rt5575->regmap, RT5575_SW_INT, ret, !ret, 100000, 10000000);
+
+ if (ret)
+ dev_err(&i2c->dev, "Firmware failure\n");
+}
+#endif
+
+static const struct snd_kcontrol_new rt5575_snd_controls[] = {
+ SOC_DOUBLE("Speaker01 Playback Switch", RT5575_SPK01_VOL, 31, 15, 1, 1),
+ SOC_DOUBLE_TLV("Speaker01 Playback Volume", RT5575_SPK01_VOL, 17, 1, 167, 0, ob_tlv),
+ SOC_DOUBLE("Speaker23 Playback Switch", RT5575_SPK23_VOL, 31, 15, 1, 1),
+ SOC_DOUBLE_TLV("Speaker23 Playback Volume", RT5575_SPK23_VOL, 17, 1, 167, 0, ob_tlv),
+ SOC_DOUBLE("Mic1 Capture Switch", RT5575_MIC1_VOL, 31, 15, 1, 1),
+ SOC_DOUBLE_TLV("Mic1 Capture Volume", RT5575_MIC1_VOL, 17, 1, 167, 0, ob_tlv),
+ SOC_DOUBLE("Mic2 Capture Switch", RT5575_MIC2_VOL, 31, 15, 1, 1),
+ SOC_DOUBLE_TLV("Mic2 Capture Volume", RT5575_MIC2_VOL, 17, 1, 167, 0, ob_tlv),
+ SOC_DOUBLE_R("Mix Playback Switch", RT5575_MIXL_VOL, RT5575_MIXR_VOL, 31, 1, 1),
+ SOC_DOUBLE_R_TLV("Mix Playback Volume", RT5575_MIXL_VOL, RT5575_MIXR_VOL, 1, 127, 0,
+ ob_tlv),
+ SOC_DOUBLE("Prompt Playback Switch", RT5575_PROMPT_VOL, 31, 15, 1, 1),
+ SOC_DOUBLE_TLV("Prompt Playback Volume", RT5575_PROMPT_VOL, 17, 1, 167, 0, ob_tlv),
+};
+
+static const struct snd_soc_dapm_widget rt5575_dapm_widgets[] = {
+ SND_SOC_DAPM_AIF_IN("AIF1RX", "AIF1 Playback", 0, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_AIF_OUT("AIF1TX", "AIF1 Capture", 0, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_AIF_IN("AIF2RX", "AIF2 Playback", 0, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_AIF_OUT("AIF2TX", "AIF2 Capture", 0, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_AIF_IN("AIF3RX", "AIF3 Playback", 0, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_AIF_OUT("AIF3TX", "AIF3 Capture", 0, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_AIF_IN("AIF4RX", "AIF4 Playback", 0, SND_SOC_NOPM, 0, 0),
+ SND_SOC_DAPM_AIF_OUT("AIF4TX", "AIF4 Capture", 0, SND_SOC_NOPM, 0, 0),
+
+ SND_SOC_DAPM_INPUT("INPUT"),
+ SND_SOC_DAPM_OUTPUT("OUTPUT"),
+};
+
+static const struct snd_soc_dapm_route rt5575_dapm_routes[] = {
+ { "AIF1TX", NULL, "INPUT" },
+ { "AIF2TX", NULL, "INPUT" },
+ { "AIF3TX", NULL, "INPUT" },
+ { "AIF4TX", NULL, "INPUT" },
+ { "OUTPUT", NULL, "AIF1RX" },
+ { "OUTPUT", NULL, "AIF2RX" },
+ { "OUTPUT", NULL, "AIF3RX" },
+ { "OUTPUT", NULL, "AIF4RX" },
+};
+
+static long long rt5575_get_priv_id(struct rt5575_priv *rt5575)
+{
+ int priv_id_low, priv_id_high;
+
+ regmap_write(rt5575->regmap, RT5575_EFUSE_PID, 0xa0000000);
+ regmap_read(rt5575->regmap, RT5575_EFUSE_DATA_2, &priv_id_low);
+ regmap_read(rt5575->regmap, RT5575_EFUSE_DATA_3, &priv_id_high);
+ regmap_write(rt5575->regmap, RT5575_EFUSE_PID, 0);
+
+ return ((long long)priv_id_high << 32) | (long long)priv_id_low;
+}
+
+static int rt5575_probe(struct snd_soc_component *component)
+{
+ struct rt5575_priv *rt5575 = snd_soc_component_get_drvdata(component);
+
+ rt5575->component = component;
+
+ dev_info(component->dev, "Private ID: %llx\n", rt5575_get_priv_id(rt5575));
+
+#if IS_ENABLED(CONFIG_SND_SOC_RT5575_SPI)
+ request_firmware_nowait(THIS_MODULE, FW_ACTION_UEVENT, "realtek/rt5575/rt5575_fw1.bin",
+ component->dev, GFP_KERNEL, rt5575, rt5575_spi_fw_loaded);
+#endif
+
+ return 0;
+}
+
+#define RT5575_STEREO_RATES SNDRV_PCM_RATE_8000_192000
+#define RT5575_FORMATS (SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE | \
+ SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S8 | \
+ SNDRV_PCM_FMTBIT_S32_LE)
+
+static struct snd_soc_dai_driver rt5575_dai[] = {
+ {
+ .name = "rt5575-aif1",
+ .id = RT5575_AIF1,
+ .playback = {
+ .stream_name = "AIF1 Playback",
+ .channels_min = 1,
+ .channels_max = 8,
+ .rates = RT5575_STEREO_RATES,
+ .formats = RT5575_FORMATS,
+ },
+ .capture = {
+ .stream_name = "AIF1 Capture",
+ .channels_min = 1,
+ .channels_max = 8,
+ .rates = RT5575_STEREO_RATES,
+ .formats = RT5575_FORMATS,
+ },
+ },
+ {
+ .name = "rt5575-aif2",
+ .id = RT5575_AIF2,
+ .playback = {
+ .stream_name = "AIF2 Playback",
+ .channels_min = 1,
+ .channels_max = 8,
+ .rates = RT5575_STEREO_RATES,
+ .formats = RT5575_FORMATS,
+ },
+ .capture = {
+ .stream_name = "AIF2 Capture",
+ .channels_min = 1,
+ .channels_max = 8,
+ .rates = RT5575_STEREO_RATES,
+ .formats = RT5575_FORMATS,
+ },
+ },
+ {
+ .name = "rt5575-aif3",
+ .id = RT5575_AIF3,
+ .playback = {
+ .stream_name = "AIF3 Playback",
+ .channels_min = 1,
+ .channels_max = 8,
+ .rates = RT5575_STEREO_RATES,
+ .formats = RT5575_FORMATS,
+ },
+ .capture = {
+ .stream_name = "AIF3 Capture",
+ .channels_min = 1,
+ .channels_max = 8,
+ .rates = RT5575_STEREO_RATES,
+ .formats = RT5575_FORMATS,
+ },
+ },
+ {
+ .name = "rt5575-aif4",
+ .id = RT5575_AIF4,
+ .playback = {
+ .stream_name = "AIF4 Playback",
+ .channels_min = 1,
+ .channels_max = 8,
+ .rates = RT5575_STEREO_RATES,
+ .formats = RT5575_FORMATS,
+ },
+ .capture = {
+ .stream_name = "AIF4 Capture",
+ .channels_min = 1,
+ .channels_max = 8,
+ .rates = RT5575_STEREO_RATES,
+ .formats = RT5575_FORMATS,
+ },
+ },
+};
+
+static const struct snd_soc_component_driver rt5575_soc_component_dev = {
+ .probe = rt5575_probe,
+ .controls = rt5575_snd_controls,
+ .num_controls = ARRAY_SIZE(rt5575_snd_controls),
+ .dapm_widgets = rt5575_dapm_widgets,
+ .num_dapm_widgets = ARRAY_SIZE(rt5575_dapm_widgets),
+ .dapm_routes = rt5575_dapm_routes,
+ .num_dapm_routes = ARRAY_SIZE(rt5575_dapm_routes),
+ .use_pmdown_time = 1,
+ .endianness = 1,
+};
+
+static const struct regmap_config rt5575_dsp_regmap = {
+ .name = "dsp",
+ .reg_bits = 32,
+ .val_bits = 32,
+ .reg_stride = 2,
+};
+
+static int rt5575_i2c_read(void *context, unsigned int reg, unsigned int *val)
+{
+ struct i2c_client *client = context;
+ struct rt5575_priv *rt5575 = i2c_get_clientdata(client);
+
+ regmap_read(rt5575->dsp_regmap, reg | RT5575_DSP_MAPPING, val);
+
+ return 0;
+}
+
+static int rt5575_i2c_write(void *context, unsigned int reg, unsigned int val)
+{
+ struct i2c_client *client = context;
+ struct rt5575_priv *rt5575 = i2c_get_clientdata(client);
+
+ regmap_write(rt5575->dsp_regmap, reg | RT5575_DSP_MAPPING, val);
+
+ return 0;
+}
+
+static const struct regmap_config rt5575_regmap = {
+ .reg_bits = 16,
+ .val_bits = 32,
+ .reg_stride = 4,
+ .max_register = 0xfffc,
+ .readable_reg = rt5575_readable_register,
+ .reg_read = rt5575_i2c_read,
+ .reg_write = rt5575_i2c_write,
+ .use_single_read = true,
+ .use_single_write = true,
+};
+
+static const struct i2c_device_id rt5575_i2c_id[] = {
+ { "rt5575" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, rt5575_i2c_id);
+
+static int rt5575_i2c_probe(struct i2c_client *i2c)
+{
+ struct rt5575_priv *rt5575;
+ int ret, val;
+
+#if IS_ENABLED(CONFIG_SND_SOC_RT5575_SPI)
+ if (!rt5575_spi_ready)
+ return -EPROBE_DEFER;
+#endif
+
+ rt5575 = devm_kzalloc(&i2c->dev, sizeof(struct rt5575_priv),
+ GFP_KERNEL);
+ if (rt5575 == NULL)
+ return -ENOMEM;
+
+ i2c_set_clientdata(i2c, rt5575);
+
+ rt5575->i2c = i2c;
+
+ rt5575->dsp_regmap = devm_regmap_init_i2c(i2c, &rt5575_dsp_regmap);
+ if (IS_ERR(rt5575->dsp_regmap)) {
+ ret = PTR_ERR(rt5575->dsp_regmap);
+ dev_err(&i2c->dev, "Failed to allocate register map: %d\n", ret);
+ return ret;
+ }
+
+ rt5575->regmap = devm_regmap_init(&i2c->dev, NULL, i2c, &rt5575_regmap);
+ if (IS_ERR(rt5575->regmap)) {
+ ret = PTR_ERR(rt5575->regmap);
+ dev_err(&i2c->dev, "Failed to allocate register map: %d\n", ret);
+ return ret;
+ }
+
+ regmap_read(rt5575->regmap, RT5575_ID, &val);
+ if (val != RT5575_DEVICE_ID) {
+ dev_err(&i2c->dev, "Device with ID register %08x is not rt5575\n", val);
+ return -ENODEV;
+ }
+
+ regmap_read(rt5575->regmap, RT5575_ID_1, &val);
+ if (!val) {
+ dev_err(&i2c->dev, "This is not formal version\n");
+ return -ENODEV;
+ }
+
+ return devm_snd_soc_register_component(&i2c->dev, &rt5575_soc_component_dev, rt5575_dai,
+ ARRAY_SIZE(rt5575_dai));
+}
+
+static const struct of_device_id rt5575_of_match[] = {
+ { .compatible = "realtek,rt5575" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, rt5575_of_match);
+
+static struct i2c_driver rt5575_i2c_driver = {
+ .driver = {
+ .name = "rt5575",
+ .owner = THIS_MODULE,
+ .of_match_table = of_match_ptr(rt5575_of_match),
+ },
+ .probe = rt5575_i2c_probe,
+ .id_table = rt5575_i2c_id,
+};
+module_i2c_driver(rt5575_i2c_driver);
+
+MODULE_DESCRIPTION("ASoC ALC5575 driver");
+MODULE_AUTHOR("Oder Chiou <oder_chiou(a)realtek.com>");
+MODULE_LICENSE("GPL");
diff --git a/sound/soc/codecs/rt5575.h b/sound/soc/codecs/rt5575.h
new file mode 100644
index 000000000000..11149612274a
--- /dev/null
+++ b/sound/soc/codecs/rt5575.h
@@ -0,0 +1,54 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * rt5575.h -- ALC5575 ALSA SoC audio driver
+ *
+ * Copyright(c) 2025 Realtek Semiconductor Corp.
+ *
+ */
+
+#ifndef __RT5575_H__
+#define __RT5575_H__
+
+#define RT5575_DEVICE_ID 0x10ec5575
+#define RT5575_DSP_MAPPING 0x18000000
+
+#define RT5575_ID 0x8008
+#define RT5575_ID_1 0x800c
+#define RT5575_MIXL_VOL 0x8a14
+#define RT5575_MIXR_VOL 0x8a18
+#define RT5575_PROMPT_VOL 0x8a84
+#define RT5575_SPK01_VOL 0x8a88
+#define RT5575_SPK23_VOL 0x8a8c
+#define RT5575_MIC1_VOL 0x8a98
+#define RT5575_MIC2_VOL 0x8a9c
+#define RT5575_WNC_CTRL 0x80ec
+#define RT5575_MODE_CTRL 0x80f0
+#define RT5575_I2S_RATE_CTRL 0x80f4
+#define RT5575_SLEEP_CTRL 0x80f8
+#define RT5575_ALG_BYPASS_CTRL 0x80fc
+#define RT5575_PINMUX_CTRL_2 0x81a4
+#define RT5575_GPIO_CTRL_1 0x8208
+#define RT5575_DSP_BUS_CTRL 0x880c
+#define RT5575_SW_INT 0x0018
+#define RT5575_DSP_BOOT_ERR 0x8e14
+#define RT5575_DSP_READY 0x8e24
+#define RT5575_DSP_CMD_ADDR 0x8e28
+#define RT5575_EFUSE_DATA_2 0xc638
+#define RT5575_EFUSE_DATA_3 0xc63c
+#define RT5575_EFUSE_PID 0xc660
+
+enum {
+ RT5575_AIF1,
+ RT5575_AIF2,
+ RT5575_AIF3,
+ RT5575_AIF4,
+ RT5575_AIFS,
+};
+
+struct rt5575_priv {
+ struct i2c_client *i2c;
+ struct snd_soc_component *component;
+ struct regmap *dsp_regmap, *regmap;
+};
+
+#endif /* __RT5575_H__ */
--
2.51.1
2
3
Raptor Lake external jack needs manual ALSA unmute and speakers don’t auto-mute
by GitHub issues - opened 31 Oct '25
by GitHub issues - opened 31 Oct '25
31 Oct '25
alsa-project/alsa-ucm-conf issue #632 was opened from keponk:
SOF HDA (skl_hda_dsp_generic) on Raptor Lake: Headphone/Line-out not exposed as a separate port to PipeWire; external jack needs manual ALSA unmute and speakers don’t auto-mute
Environment (for quick identification)
* Distro: NixOS (current channel)
* Audio stack: PipeWire/WirePlumber 1.4.7, PipeWire Pulse
* Hardware: Intel Raptor Lake PCH HDA (onboard), SOF HDA path (skl_hda_dsp_generic)
* Symptom device string seen in userspace: “Raptor Lake High Definition Audio Controller”
* PipeWire sink name: alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__Speaker__sink
Problem summary
* The 3.5 mm headphone/line-out jack produces no audio unless the “Headphone” control is manually unmuted via ALSA (alsamixer/amixer).
* PipeWire/WirePlumber only expose a single output port (“Speaker”) for this card/profile; there is no separate “Headphones/Line Out” port to select in GNOME/pavucontrol.
* Internal speakers do not auto-mute when the external jack is used; some intermittent audio still leaks to the internal speakers.
* Jack detection appears unstable (repeated “Headphone Jack” events seen) and the desktop toggles output availability intermittently.
Expected outcome
* PipeWire/WirePlumber should expose distinct output ports for Speaker and Headphones/Line Out, based on UCM, for this device.
* When the 3.5 mm plug is inserted, the system should:
1. Auto-switch to the Headphones/Line Out port,
1. Mute the internal speakers (auto-mute),
1. Keep the state stable (no flapping).
1. Users should not need to manipulate raw ALSA mixer controls manually for basic jack usage.
Actual outcome
* Only “Speaker” is exposed as an output port; no “Headphones/Line Out” port is available to select.
* Audio plays intermittently through internal speakers; external jack is silent until “Headphone” is manually unmuted in alsamixer.
* After manual unmute, the external jack works, but internal speakers continue to output intermittently unless manually muted; auto-mute does not engage.
* Jack-sense events appear to flap while the plug is steady, causing intermittent switching in the desktop.
Steps to reproduce (high level)
* Boot system; log in to GNOME (PipeWire/WirePlumber active).
* Plug powered external speakers/headphones into the 3.5 mm combo jack.
* Observe that "Headphone" output intermittently appears on the dropdown. Hard to select via UI, but when is done, the sound comes out of the internal speakers only, in synchrony with the intermittence in the ui.
* Use alsamixer on the SOF HDA card to unmute “Headphone”; external jack starts working, but internal speakers still play unless manually muted; behavior may fluctuate with jack events.
Impact:
* Users cannot rely on auto-detection or desktop controls for basic headphone/line-out use. Manual ALSA mixer changes are required. Internal speakers may still play, causing privacy/UX issues.
Workaround:
* Manually unmute “Headphone” and set “Speaker” to lowest volume in alsamixer; Auto-Mute doesn't seem to work in this setup.
* Disabling HDA power saving didn't seem to help reduce jack flapping.
Possble solution:
In alsa-ucm-conf for the SOF HDA “skl_hda_dsp_generic” profile on this Raptor Lake codec:
* Define and expose a proper “Headphones/Line Out” device/port alongside “Speaker.”
* Add the appropriate Enable/Disable/Jack sequences to unmute Headphone, mute Speaker (auto-mute), and handle jack detection correctly.
* If the UCM is already correct for this codec, guidance is appreciated on whether the issue should be addressed in:
- ALSA kernel (snd_hda_intel/Realtek codec quirk) or SOF topology/firmware for stable jack sense and correct control exposure.
- WirePlumber/PipeWire only if UCM provides the ports but they are not shown.
alsa-info: http://alsa-project.org/db/?f=357e5def55a9a2e737306ff2ec22b1526a14f87a
Issue URL : https://github.com/alsa-project/alsa-ucm-conf/issues/632
Repository URL: https://github.com/alsa-project/alsa-ucm-conf
3
2
The following changes since commit 211ddde0823f1442e4ad052a2f30f050145ccada:
Linux 6.18-rc2 (2025-10-19 15:19:16 -1000)
are available in the Git repository at:
https://git.kernel.org/pub/scm/linux/kernel/git/broonie/sound.git tags/asoc-fix-v6.18-rc2
for you to fetch changes up to 5e5c8aa73d99f1daa9f2ec1474b7fc1a6952764b:
ASoC: dt-bindings: pm4125-sdw: correct number of soundwire ports (2025-10-29 14:54:47 +0000)
----------------------------------------------------------------
ASoC: Fixes for v6.18
A bigger batch of fixes than I'd like, things built up due to holidays
and some last minute issues which caused me to hold off on sending a pul
request. None of these are super remarkable, and there's a few new
device IDs in here too including a relatively big block of AMD devices.
The Cirrus Logic CS530x support subject line is actually a fix that was
on the start of that series and got pulled in here, I forgot to fix the
subject up when merging.
----------------------------------------------------------------
Bard Liao (1):
ASoC: soc_sdw_utils: remove cs42l43 component_name
Cezary Rojewski (3):
ASoC: Intel: avs: Unprepare a stream when XRUN occurs
ASoC: Intel: avs: Disable periods-elapsed work when closing PCM
ASoC: Intel: avs: Use snd_codec format when initializing probe
Claudiu Beznea (1):
ASoC: renesas: rz-ssi: Use proper dma_buffer_pos after resume
Haotian Zhang (1):
ASoC: mediatek: Fix double pm_runtime_disable in remove functions
Maarten Zanders (1):
ASoC: fsl_sai: Fix sync error in consumer mode
Mark Brown (4):
Add support for Cirrus Logic CS530x DAC and CODEC
ASoC: Intel: avs: Set of streaming fixes
ASoC: fsl: correct the bit order issue for DSD
ASoC: Fix build for sdw_utils
Richard Fitzgerald (1):
ASoC: cs-amp-lib-test: Fix missing include of kunit/test-bug.h
Sharique Mohammad (1):
ASOC: max98090/91: fix for filter configuration: AHPF removed DMIC2_HPF added
Shengjiu Wang (2):
ASoC: fsl_sai: fix bit order for DSD format
ASoC: fsl_micfil: correct the endian format for DSD
Shuming Fan (2):
ASoC: sdw_utils: add name_prefix for rt1321 part id
ASoC: rt721: fix prepare clock stop failed
Simon Trimmer (3):
ASoC: cs530x: Correct log message with expected variable
ASoC: amd: acp: Add ACP7.0 match entries for cs35l56 and cs42l43
ASoC: Intel: soc-acpi-intel-ptl-match: Remove cs42l43 match from sdw link3
Srinivas Kandagatla (2):
ASoC: qdsp6: q6asm: do not sleep while atomic
ASoC: dt-bindings: pm4125-sdw: correct number of soundwire ports
.../devicetree/bindings/sound/qcom,pm4125-sdw.yaml | 4 +-
sound/soc/amd/acp/amd-acp70-acpi-match.c | 157 +++++++++++++++++++++
sound/soc/codecs/cs-amp-lib-test.c | 1 +
sound/soc/codecs/cs530x.c | 2 +-
sound/soc/codecs/max98090.c | 6 +-
sound/soc/codecs/rt721-sdca.c | 4 +
sound/soc/codecs/rt721-sdca.h | 1 +
sound/soc/fsl/fsl_micfil.c | 4 +-
sound/soc/fsl/fsl_sai.c | 11 +-
sound/soc/intel/avs/pcm.c | 3 +
sound/soc/intel/avs/probes.c | 18 +--
sound/soc/intel/common/soc-acpi-intel-ptl-match.c | 52 -------
sound/soc/mediatek/mt8195/mt8195-afe-pcm.c | 1 -
sound/soc/mediatek/mt8365/mt8365-afe-pcm.c | 1 -
sound/soc/qcom/qdsp6/q6asm.c | 2 +-
sound/soc/renesas/rz-ssi.c | 25 ++--
sound/soc/sdw_utils/soc_sdw_utils.c | 1 -
17 files changed, 203 insertions(+), 90 deletions(-)
2
1
alsa-project/alsa-utils issue #300 was opened from michaelforney:
When I use `aseqsend` to send a sysex message to a device, it doesn't seem to work, though there is no error message. Running with strace, it seems it is failing with ENODEV.
```sh
$ strace aseqsend -p 32:0 'F0 43 7D 10 41 30 01 00 00 01 F7'
...
open("/dev/snd/seq", O_WRONLY|O_LARGEFILE|O_CLOEXEC) = 3
fcntl(3, F_SETFD, FD_CLOEXEC) = 0
ioctl(3, SNDRV_SEQ_IOCTL_PVERSION, 0x7fffef1ae678) = 0
ioctl(3, SNDRV_SEQ_IOCTL_USER_PVERSION, 0x7fffef1ae67c) = 0
mmap(NULL, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f6ae25f7000
ioctl(3, SNDRV_SEQ_IOCTL_CLIENT_ID, 0x7fffef1ae67c) = 0
ioctl(3, SNDRV_SEQ_IOCTL_RUNNING_MODE, 0x7fffef1ae680) = 0
ioctl(3, SNDRV_SEQ_IOCTL_GET_CLIENT_INFO, 0x7fffef1ae940) = 0
ioctl(3, SNDRV_SEQ_IOCTL_SET_CLIENT_INFO, 0x7fffef1ae940) = 0
ioctl(3, SNDRV_SEQ_IOCTL_GET_CLIENT_INFO, 0x7fffef1ae940) = 0
ioctl(3, SNDRV_SEQ_IOCTL_SET_CLIENT_INFO, 0x7fffef1ae940) = 0
ioctl(3, SNDRV_SEQ_IOCTL_CREATE_PORT, 0x7fffef1ae960) = 0
write(3, "\202\4\0\375\0\0\0\0\0\0\0\0\0\0 \0\v\0\0\0\320\212WN\361U\0\0\360C}\20"..., 39) = -1 ENODEV (No such device)
nanosleep({tv_sec=0, tv_nsec=1000000}, 0x7fffef1aea20) = 0
close(3) = 0
munmap(0x7f6ae25f7000, 20480) = 0
exit_group(0) = ?
+++ exited with 0 +++
$
```
`amidi -S ...` works as expected. If I modify `aseqsend` to first subscribe to the port, it seems to work:
```diff
diff --git a/seq/aseqsend/aseqsend.c b/seq/aseqsend/aseqsend.c
index 92354eb..ad0a636 100644
--- a/seq/aseqsend/aseqsend.c
+++ b/seq/aseqsend/aseqsend.c
@@ -364,6 +364,7 @@ int main(int argc, char *argv[])
char do_port_list = 0;
char verbose = 0;
int k;
+ int err;
while ((c = getopt_long(argc, argv, "hi:Vvlp:s:u:", long_options, NULL)) != -1) {
switch (c) {
@@ -439,6 +440,8 @@ int main(int argc, char *argv[])
error("Unable to parse port name!");
exit(EXIT_FAILURE);
}
+ err = snd_seq_connect_to(seq, 0, addr.client, addr.port);
+ check_snd("connect to port", err);
sent_data_c = 0; //counter of actually sent bytes
```
From what I've read, I don't think it should be necessary to subscribe to a port to send it messages. However, I did find one sentence in the alsa-lib docs that seem to indicate that it is needed for hardware ports:
> There is another subscription type for opposite direction: Suppose a MIDI sequencer program which sends events to a MIDI output device. In ALSA system, MIDI device is not opened until the associated MIDI port is accessed. Thus, in order to activate MIDI device, we have to subscribe to MIDI port for write. After this connection is established, events will be properly sent to MIDI output device.
This seems to be a pretty basic use of `aseqsend`, so I am a bit surprised that it wasn't working. Is the subscription the right way to fix the problem, or am I doing something else wrong?
Issue URL : https://github.com/alsa-project/alsa-utils/issues/300
Repository URL: https://github.com/alsa-project/alsa-utils
2
1
[PATCH AUTOSEL 6.1 02/13] ASoC: amd: yc: Add quirk for Lenovo Yoga Pro 7 14ASP9
by Sasha Levin 30 Oct '25
by Sasha Levin 30 Oct '25
30 Oct '25
From: Talhah Peerbhai <talhah.peerbhai(a)gmail.com>
[ Upstream commit a28206060dc5848a1a2a15b7f6ac6223d869084d ]
Similar to many other Lenovo models with AMD chips, the Lenovo
Yoga Pro 7 14ASP9 (product name 83HN) requires a specific quirk
to ensure internal mic detection. This patch adds a quirk fixing this.
Signed-off-by: Talhah Peerbhai <talhah.peerbhai(a)gmail.com>
Link: https://patch.msgid.link/20250515222741.144616-1-talhah.peerbhai@gmail.com
Signed-off-by: Mark Brown <broonie(a)kernel.org>
Signed-off-by: Sasha Levin <sashal(a)kernel.org>
---
Based on my analysis of this commit, I can provide a comprehensive
assessment for backporting. **YES** This commit should be backported to
stable kernel trees for the following reasons: **1. Identical Pattern to
Previously Backported Commits:** All five similar commits provided in
the historical context (commits for Lenovo models 82UU, 82, 82YM, 83AS,
and 21M1) have "Backport Status: YES" and follow the exact same pattern
as this commit. This establishes a clear precedent for backporting such
changes. **2. Hardware Enablement Bug Fix:** The commit fixes a clear
user-affecting bug where the internal microphone on the Lenovo Yoga Pro
7 14ASP9 (product name 83HN) is non-functional. This is a hardware
compatibility issue that directly impacts users' ability to use their
device's built-in microphone. **3. Minimal and Contained Change:** The
code change is extremely small and safe: - Adds only a single 7-line
quirk table entry to `sound/soc/amd/yc/acp6x-mach.c` - The change is
purely additive - no existing code is modified - The quirk entry follows
the exact same pattern as dozens of other entries in the same table -
Zero risk of regression to existing functionality **4. Critical
Subsystem with Safe Pattern:** While this touches audio (a critical
subsystem), the change follows a well-established, safe pattern used
extensively in this driver. The quirk table approach is designed
specifically for adding device-specific compatibility without affecting
other hardware. **5. Clear User Benefit:** Users with this specific
Lenovo model will have their internal microphone functionality restored,
which is essential for video calls, voice recording, and other audio
input tasks. **6. Code Quality Improvement:** The commit also includes a
minor whitespace fix (changing spaces to tab at line 350), improving
code formatting consistency. **7. Follows Stable Tree Rules:** -
Important bugfix: ✓ (enables hardware functionality) - Minimal risk: ✓
(purely additive quirk entry) - Small and contained: ✓ (7 lines added) -
No architectural changes: ✓ - Confined to subsystem: ✓ (AMD YC audio
driver) The commit message clearly explains the issue and solution, and
the change is identical in nature to numerous other successfully
backported commits for similar Lenovo audio quirks. This represents a
textbook example of a stable-appropriate hardware enablement fix.
sound/soc/amd/yc/acp6x-mach.c | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/sound/soc/amd/yc/acp6x-mach.c b/sound/soc/amd/yc/acp6x-mach.c
index 1f94269e121af..d5dc1d48fca94 100644
--- a/sound/soc/amd/yc/acp6x-mach.c
+++ b/sound/soc/amd/yc/acp6x-mach.c
@@ -304,6 +304,13 @@ static const struct dmi_system_id yc_acp_quirk_table[] = {
DMI_MATCH(DMI_PRODUCT_NAME, "83AS"),
}
},
+ {
+ .driver_data = &acp6x_card,
+ .matches = {
+ DMI_MATCH(DMI_BOARD_VENDOR, "LENOVO"),
+ DMI_MATCH(DMI_PRODUCT_NAME, "83HN"),
+ }
+ },
{
.driver_data = &acp6x_card,
.matches = {
@@ -353,7 +360,7 @@ static const struct dmi_system_id yc_acp_quirk_table[] = {
DMI_MATCH(DMI_PRODUCT_NAME, "M5402RA"),
}
},
- {
+ {
.driver_data = &acp6x_card,
.matches = {
DMI_MATCH(DMI_BOARD_VENDOR, "ASUSTeK COMPUTER INC."),
--
2.39.5
2
1
[alsa-devel] Applied "ASoC: tlv320aic32x4: Model CODEC_CLKIN in CCF" to the asoc tree
by Mark Brown 29 Oct '25
by Mark Brown 29 Oct '25
29 Oct '25
The patch
ASoC: tlv320aic32x4: Model CODEC_CLKIN in CCF
has been applied to the asoc tree at
https://git.kernel.org/pub/scm/linux/kernel/git/broonie/sound.git
All being well this means that it will be integrated into the linux-next
tree (usually sometime in the next 24 hours) and sent to Linus during
the next merge window (or sooner if it is a bug fix), however if
problems are discovered then the patch may be dropped or reverted.
You may get further e-mails resulting from automated or manual testing
and review of the tree, please engage with people reporting problems and
send followup patches addressing any issues that are reported if needed.
If any updates are required or you are submitting further changes they
should be sent as incremental updates against current git, existing
patches will not be replaced.
Please add any relevant lists and maintainers to the CCs when replying
to this mail.
Thanks,
Mark
>From fd2df3aeafa4b4cc468d58e147e0822967034b71 Mon Sep 17 00:00:00 2001
From: Annaliese McDermond <nh6z(a)nh6z.net>
Date: Thu, 21 Mar 2019 17:58:46 -0700
Subject: [PATCH] ASoC: tlv320aic32x4: Model CODEC_CLKIN in CCF
Model and manage codec clock input as a component in the Core
Clock Framework. This should allow us to do some more complex
clock management and power control. Also, some of the
on-board chip clocks can be exposed to the outside, and this
change will make those clocks easier to consume by other
parts of the kernel.
Signed-off-by: Annaliese McDermond <nh6z(a)nh6z.net>
Signed-off-by: Mark Brown <broonie(a)kernel.org>
---
sound/soc/codecs/tlv320aic32x4-clk.c | 34 ++++++++++++++++++++++++++++
sound/soc/codecs/tlv320aic32x4.c | 18 +++++++++++----
2 files changed, 47 insertions(+), 5 deletions(-)
diff --git a/sound/soc/codecs/tlv320aic32x4-clk.c b/sound/soc/codecs/tlv320aic32x4-clk.c
index 5e495fc8d931..cded85009c8c 100644
--- a/sound/soc/codecs/tlv320aic32x4-clk.c
+++ b/sound/soc/codecs/tlv320aic32x4-clk.c
@@ -265,6 +265,30 @@ static const struct clk_ops aic32x4_pll_ops = {
.get_parent = clk_aic32x4_pll_get_parent,
};
+static int clk_aic32x4_codec_clkin_set_parent(struct clk_hw *hw, u8 index)
+{
+ struct clk_aic32x4 *mux = to_clk_aic32x4(hw);
+
+ return regmap_update_bits(mux->regmap,
+ AIC32X4_CLKMUX,
+ AIC32X4_CODEC_CLKIN_MASK, index << AIC32X4_CODEC_CLKIN_SHIFT);
+}
+
+static u8 clk_aic32x4_codec_clkin_get_parent(struct clk_hw *hw)
+{
+ struct clk_aic32x4 *mux = to_clk_aic32x4(hw);
+ unsigned int val;
+
+ regmap_read(mux->regmap, AIC32X4_CLKMUX, &val);
+
+ return (val & AIC32X4_CODEC_CLKIN_MASK) >> AIC32X4_CODEC_CLKIN_SHIFT;
+}
+
+static const struct clk_ops aic32x4_codec_clkin_ops = {
+ .set_parent = clk_aic32x4_codec_clkin_set_parent,
+ .get_parent = clk_aic32x4_codec_clkin_get_parent,
+};
+
static struct aic32x4_clkdesc aic32x4_clkdesc_array[] = {
{
.name = "pll",
@@ -274,6 +298,14 @@ static struct aic32x4_clkdesc aic32x4_clkdesc_array[] = {
.ops = &aic32x4_pll_ops,
.reg = 0,
},
+ {
+ .name = "codec_clkin",
+ .parent_names =
+ (const char *[]) { "mclk", "bclk", "gpio", "pll" },
+ .num_parents = 4,
+ .ops = &aic32x4_codec_clkin_ops,
+ .reg = 0,
+ },
};
static struct clk *aic32x4_register_clk(struct device *dev,
@@ -314,6 +346,8 @@ int aic32x4_register_clocks(struct device *dev, const char *mclk_name)
*/
aic32x4_clkdesc_array[0].parent_names =
(const char* []) { mclk_name, "bclk", "gpio", "din" };
+ aic32x4_clkdesc_array[1].parent_names =
+ (const char *[]) { mclk_name, "bclk", "gpio", "pll" };
for (i = 0; i < ARRAY_SIZE(aic32x4_clkdesc_array); ++i)
aic32x4_register_clk(dev, &aic32x4_clkdesc_array[i]);
diff --git a/sound/soc/codecs/tlv320aic32x4.c b/sound/soc/codecs/tlv320aic32x4.c
index 7cf8c7cedfe1..5496e4e080f4 100644
--- a/sound/soc/codecs/tlv320aic32x4.c
+++ b/sound/soc/codecs/tlv320aic32x4.c
@@ -735,12 +735,9 @@ static int aic32x4_setup_clocks(struct snd_soc_component *component,
aic32x4_set_processing_blocks(component, aic32x4_divs[i].r_block, aic32x4_divs[i].p_block);
- /* PLL as CODEC_CLKIN */
- snd_soc_component_update_bits(component, AIC32X4_CLKMUX,
- AIC32X4_CODEC_CLKIN_MASK,
- AIC32X4_CODEC_CLKIN_PLL << AIC32X4_CODEC_CLKIN_SHIFT);
/* DAC_MOD_CLK as BDIV_CLKIN */
- snd_soc_component_update_bits(component, AIC32X4_IFACE3, AIC32X4_BDIVCLK_MASK,
+ snd_soc_component_update_bits(component, AIC32X4_IFACE3,
+ AIC32X4_BDIVCLK_MASK,
AIC32X4_DACMOD2BCLK << AIC32X4_BDIVCLK_SHIFT);
/* NDAC divider value */
@@ -987,6 +984,15 @@ static int aic32x4_component_probe(struct snd_soc_component *component)
{
struct aic32x4_priv *aic32x4 = snd_soc_component_get_drvdata(component);
u32 tmp_reg;
+ int ret;
+
+ struct clk_bulk_data clocks[] = {
+ { .id = "codec_clkin" },
+ };
+
+ ret = devm_clk_bulk_get(component->dev, ARRAY_SIZE(clocks), clocks);
+ if (ret)
+ return ret;
if (gpio_is_valid(aic32x4->rstn_gpio)) {
ndelay(10);
@@ -999,6 +1005,8 @@ static int aic32x4_component_probe(struct snd_soc_component *component)
if (aic32x4->setup)
aic32x4_setup_gpios(component);
+ clk_set_parent(clocks[0].clk, clocks[1].clk);
+
/* Power platform configuration */
if (aic32x4->power_cfg & AIC32X4_PWR_MICBIAS_2075_LDOIN) {
snd_soc_component_write(component, AIC32X4_MICBIAS,
--
2.20.1
6
7
alsa-project/alsa-ucm-conf pull request #631 was edited from mohsRafi:
Add UCM2 configs for the Qualcomm TALOS-EVK Board.
Request URL : https://github.com/alsa-project/alsa-ucm-conf/pull/631
Patch URL : https://github.com/alsa-project/alsa-ucm-conf/pull/631.patch
Repository URL: https://github.com/alsa-project/alsa-ucm-conf
1
0