#2356 Audio playback redesign. Resolves audio playback issue on some (e.g. Kubuntu) systems. For stereo playback, (#2359)

consolidates left and right channels into a single playback channel.  Implements continuous audio streaming using
silence to fill the gaps to avoid mixer source data line start/stop issues.  Code structure is ready to layer on
multi-channel (ie 5.1 or 7.1) audio playback devices, but that hasn't been tested.

Co-authored-by: denny <dennis.sheirer@gmail.com>
This commit is contained in:
Denny Sheirer
2026-02-08 05:30:39 -05:00
committed by GitHub
parent a2509554db
commit c81e6cccfa
21 changed files with 1817 additions and 1220 deletions

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2022 Dennis Sheirer
* Copyright (C) 2014-2026 Dennis Sheirer
*
* 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
@@ -20,8 +20,6 @@ package io.github.dsheirer.audio.playback;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.audio.AudioEvent;
import io.github.dsheirer.audio.AudioException;
import io.github.dsheirer.audio.IAudioController;
import io.github.dsheirer.eventbus.MyEventBus;
import io.github.dsheirer.gui.preference.PreferenceEditorType;
import io.github.dsheirer.gui.preference.ViewUserPreferenceEditorRequest;
@@ -29,81 +27,77 @@ import io.github.dsheirer.icon.IconModel;
import io.github.dsheirer.preference.UserPreferences;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.mixer.MixerChannelConfiguration;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import jiconfont.icons.font_awesome.FontAwesome;
import jiconfont.swing.IconFontSwing;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.FloatControl;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
/**
* Audio playback panel
*/
public class AudioPanel extends JPanel implements Listener<AudioEvent>
{
private static final long serialVersionUID = 1L;
private static final Logger mLog = LoggerFactory.getLogger(AudioPanel.class);
private static ImageIcon MUTED_ICON = IconModel.getScaledIcon("images/audio_muted.png", 20);
private static ImageIcon UNMUTED_ICON = IconModel.getScaledIcon("images/audio_unmuted.png", 20);
private IconModel mIconModel;
private SettingsManager mSettingsManager;
private IAudioController mController;
private AliasModel mAliasModel;
private UserPreferences mUserPreferences;
private JButton mMuteButton;
private static final ImageIcon MUTED_ICON = IconModel.getScaledIcon("images/audio_muted.png", 20);
private static final ImageIcon UNMUTED_ICON = IconModel.getScaledIcon("images/audio_unmuted.png", 20);
private final AliasModel mAliasModel;
private final AudioPlaybackManager mAudioPlaybackManager;
private final IconModel mIconModel;
private final SettingsManager mSettingsManager;
private final UserPreferences mUserPreferences;
private AudioChannelsPanel mAudioChannelsPanel;
private JButton mMuteButton;
/**
* Constructs an instance
* @param iconModel for icon lookup
* @param userPreferences for preference lookup
* @param settingsManager to monitor for changes
* @param audioPlaybackManager for accessing the audio output
* @param aliasModel for alias lookup
*/
public AudioPanel(IconModel iconModel, UserPreferences userPreferences, SettingsManager settingsManager,
IAudioController controller, AliasModel aliasModel)
AudioPlaybackManager audioPlaybackManager, AliasModel aliasModel)
{
mIconModel = iconModel;
mSettingsManager = settingsManager;
mController = controller;
mAudioPlaybackManager = audioPlaybackManager;
mAliasModel = aliasModel;
mUserPreferences = userPreferences;
mController.addControllerListener(this);
mAudioPlaybackManager.addAudioEventListener(this);
init();
}
/**
* Initialize the display
*/
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0", "[]0[grow,fill]", "[fill]0[]"));
setBackground(Color.BLACK);
mMuteButton = new MuteButton();
mMuteButton.setBackground(getBackground());
add(mMuteButton);
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mAudioPlaybackManager, mAliasModel);
add(mAudioChannelsPanel);
addMouseListener(new MouseSelectionListener());
}
/**
* Receive audio event notifications from the audio playback controller
*/
@Override
public void receive(AudioEvent event)
{
@@ -112,19 +106,14 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
case AUDIO_CONFIGURATION_CHANGE_STARTED:
break;
case AUDIO_CONFIGURATION_CHANGE_COMPLETE:
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
remove(mAudioChannelsPanel);
mAudioChannelsPanel.dispose();
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mController, mAliasModel);
add(mAudioChannelsPanel);
mAudioChannelsPanel.repaint();
revalidate();
repaint();
}
EventQueue.invokeLater(() -> {
remove(mAudioChannelsPanel);
mAudioChannelsPanel.dispose();
mAudioChannelsPanel = new AudioChannelsPanel(mIconModel, mUserPreferences, mSettingsManager, mAudioPlaybackManager, mAliasModel);
add(mAudioChannelsPanel);
mAudioChannelsPanel.repaint();
revalidate();
repaint();
});
break;
default:
@@ -135,26 +124,19 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
/**
* Audio output mute control menu item.
*/
public class AudioOutputMuteItem extends JMenuItem
public static class AudioOutputMuteItem extends JMenuItem
{
private static final long serialVersionUID = 1L;
private AudioOutput mAudioOutput;
private final AudioOutput mAudioOutput;
/**
* Constructs an instance
* @param audioOutput to mute/unmute
*/
public AudioOutputMuteItem(AudioOutput audioOutput)
{
super(audioOutput.isMuted() ? "Unmute" : "Mute");
mAudioOutput = audioOutput;
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
mAudioOutput.setMuted(!mAudioOutput.isMuted());
}
});
addActionListener(e -> mAudioOutput.setMuted(!mAudioOutput.isMuted()));
}
}
@@ -169,37 +151,23 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
if(SwingUtilities.isRightMouseButton(event))
{
JPopupMenu popup = new JPopupMenu();
/* Audio mixer/output selection menus */
JMenuItem outputMenu = new JMenuItem("Configure ...");
JMenuItem outputMenu = new JMenuItem("Audio Playback Device ...");
Icon icon = IconFontSwing.buildIcon(FontAwesome.COG, 14);
outputMenu.setIcon(icon);
outputMenu.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
MyEventBus.getGlobalEventBus().post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT));
}
});
outputMenu.addActionListener(e -> MyEventBus.getGlobalEventBus()
.post(new ViewUserPreferenceEditorRequest(PreferenceEditorType.AUDIO_OUTPUT)));
popup.add(outputMenu);
popup.add(new JPopupMenu.Separator());
/* Audio output mute and volume control */
for(AudioOutput output : mController.getAudioOutputs())
//Add optional gain controller for th eaudio output
if(mAudioPlaybackManager.getAudioOutput() != null && mAudioPlaybackManager.getAudioOutput().hasGainControl())
{
JMenu menu = new JMenu("Channel: " + output.getChannelName());
menu.add(new AudioOutputMuteItem(output));
if(output.hasGainControl())
{
JMenu volume = new JMenu("Volume");
volume.add(new VolumeSlider(output.getGainControl()));
menu.add(volume);
}
popup.add(menu);
popup.add(new JPopupMenu.Separator());
JMenuItem volume = new JMenuItem("Audio Volume");
volume.setEnabled(false);
Icon volumeIcon = IconFontSwing.buildIcon(FontAwesome.VOLUME_UP, 14);
volume.setIcon(volumeIcon);
popup.add(volume);
popup.add(new VolumeSlider(mAudioPlaybackManager.getAudioOutput().getGainControl()));
}
popup.show(event.getComponent(), event.getX(), event.getY());
@@ -230,12 +198,14 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
/**
* Audio volume (gain) adjustment slider control
*/
public class VolumeSlider extends JSlider
public static class VolumeSlider extends JSlider
{
private static final long serialVersionUID = 1L;
private FloatControl mFloatControl;
private final FloatControl mFloatControl;
/**
* Constructs an instance
* @param control to be controlled
*/
public VolumeSlider(FloatControl control)
{
super(0, 100, 0);
@@ -244,21 +214,11 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
setMinorTickSpacing(5);
setPaintTicks(true);
setPaintLabels(true);
mFloatControl = control;
setValue(getIntegerValue(mFloatControl.getValue()));
addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(ChangeEvent event)
{
mFloatControl.shift(mFloatControl.getValue(),
getFloatValue(VolumeSlider.this.getValue()),
1000);
}
});
addChangeListener(event -> mFloatControl.shift(mFloatControl.getValue(),
getFloatValue(VolumeSlider.this.getValue()),
1000));
addMouseListener(new MouseListener()
{
@@ -270,22 +230,10 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
VolumeSlider.this.setValue(50);
}
}
public void mouseReleased(MouseEvent arg0)
{
}
public void mousePressed(MouseEvent arg0)
{
}
public void mouseExited(MouseEvent arg0)
{
}
public void mouseEntered(MouseEvent arg0)
{
}
public void mouseReleased(MouseEvent arg0) {}
public void mousePressed(MouseEvent arg0) {}
public void mouseExited(MouseEvent arg0) {}
public void mouseEntered(MouseEvent arg0) {}
});
}
@@ -331,52 +279,12 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
}
}
/**
* Mixer/Channel configuration selection item
*/
public class MixerSelectionItem extends JMenuItem
{
private static final long serialVersionUID = 1L;
private MixerChannelConfiguration mConfiguration;
public MixerSelectionItem(MixerChannelConfiguration config)
{
super(config.toString());
mConfiguration = config;
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
try
{
mController.setMixerChannelConfiguration(mConfiguration);
}
catch(AudioException e1)
{
mLog.error("Couldn't set mixer channel configuration "
+ "to: " + mConfiguration.toString());
JOptionPane.showMessageDialog(MixerSelectionItem.this,
"Couldn't set [" + mConfiguration.toString() +
"] as the audio output device");
}
}
});
}
}
/**
* Mute button to mute all audio output channels exposed by the audio
* controller
*/
public class MuteButton extends JButton
{
private static final long serialVersionUID = 1L;
private boolean mMuted = false;
public MuteButton()
@@ -384,29 +292,13 @@ public class AudioPanel extends JPanel implements Listener<AudioEvent>
setIcon(UNMUTED_ICON);
setBorderPainted(false);
getAccessibleContext().setAccessibleName("Mute");
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
mMuted = !mMuted;
for(AudioOutput output : mController.getAudioOutputs())
{
output.setMuted(mMuted);
}
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
setIcon(mMuted ? MUTED_ICON : UNMUTED_ICON);
getAccessibleContext().setAccessibleName(mMuted ? "Unmute" : "Mute");
}
});
}
addActionListener(e -> {
mMuted = !mMuted;
mAudioPlaybackManager.getAudioOutput().setMuted(mMuted);
EventQueue.invokeLater(() -> {
setIcon(mMuted ? MUTED_ICON : UNMUTED_ICON);
getAccessibleContext().setAccessibleName(mMuted ? "Unmute" : "Mute");
});
});
}
}