0

I'm playing around with javax.sound libraries for the first time and I'm trying to record audio that is currently playing through my computer's default device, such as music or a video (not a microphone).

I might just be getting myself confused between TargetDataLine and SourceDataLine, but no matter what I try, all I can manage to do is record sound from the microphone.

import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.sound.sampled.*;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

@Slf4j
@Service
public class AudioServiceImpl implements AudioService {

    private final File wavFile = new File("output.wav");

    private final AudioFileFormat.Type fileType = AudioFileFormat.Type.WAVE;

    private TargetDataLine line;

    @Override
    public void start() {

        final Mixer defaultMixer = getMixersWithAudioFormats().stream()
                .filter(mixer -> mixer.getMixerInfo().getName().equals("Default Audio Device"))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("No default audio device found"));

        try {
            final AudioInterface audioInterface = getAudioFormat(defaultMixer);

            line = (TargetDataLine) AudioSystem.getLine(audioInterface.info());
            line.open(line.getFormat());
            line.start();

            AudioInputStream audioInputStream = new AudioInputStream(line);

            log.info("Recording...");

            AudioSystem.write(audioInputStream, fileType, wavFile);

        } catch (LineUnavailableException | IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Find the first audio format where the line is supported.
     */
    private AudioInterface getAudioFormat(final Mixer mixer) {
        for (final Line.Info mixerInfo : mixer.getTargetLineInfo()) {
            final AudioFormat[] formats = getFormats(mixerInfo);

            for (final AudioFormat format : formats) {

                DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);

                if (AudioSystem.isLineSupported(info)) {
                    return new AudioInterface(format, info);
                }
            }
        }

        throw new RuntimeException("No supported audio format found");
    }

    /**
     * Find all mixers with audio formats.
     */
    private List<Mixer> getMixersWithAudioFormats() {
        final List<Mixer> results = new LinkedList<>();

        for (Mixer.Info info : AudioSystem.getMixerInfo()) {
            final Mixer mixer = AudioSystem.getMixer(info);
            final Line.Info[] lineInfos = mixer.getTargetLineInfo();

            for (Line.Info lineInfo : lineInfos) {
                if ((lineInfo instanceof final DataLine.Info dataLineInfo) && (dataLineInfo.getFormats().length > 0)) {
                    results.add(mixer);
                }
            }
        }

        return results;
    }

    /**
     * Helper to grab audio formats from a line info.
     */
    private AudioFormat[] getFormats(final Line.Info lineInfo) {
        return lineInfo instanceof final DataLine.Info dataLineInfo ? dataLineInfo.getFormats() : null;
    }

    @PreDestroy
    void finish() {
        line.stop();
        line.close();
    }
}

The AudioInterface is just a helper record

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;

public record AudioInterface(AudioFormat format, DataLine.Info info) {
}

My understanding is that I'm correctly grabbing the Default Audio Device mixer, extracting the format out of it and loading that into the AudioSystem.getLine method. I just would have thought that to end up recording from the microphone I'd have to have been using a different mixer (In my case, MacBook Pro Microphone) instead.

Where have I gone wrong here?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Chris
  • 3,437
  • 6
  • 40
  • 73
  • Javasound will only give you access to input from the microphone and output to speakers. It will not let you access output from other programs going to the speakers - because most operating systems don't provide a simple way to do that, and it would also be a security issue. – greg-449 Feb 15 '22 at 08:55
  • I'd argue allowing access to the microphone would be a much larger risk than simply the audio stream being sent to the output... anyway; I've seen a bunch about being able to use the Mixer class and different ports (headphone, speaker etc) to intercept data, adjust volume etc. I don't believe it's "impossible" within the JVM or via JNI to tap into this. – Chris Feb 15 '22 at 08:58
  • Like I said most operating systems simply do not allow one process to intercept audio output from another process. Programs that do manage that have to hook in to the OS in some way that normally requires special permissions, or installation the JVM does not do that. – greg-449 Feb 15 '22 at 09:05
  • And yes, microphone access is a security issue. I use macOS and it requires authorisation before allowing the JVM (or anything else) to use the microphone. It also shows a visible indicator when it is in use. – greg-449 Feb 15 '22 at 09:08
  • Thanks for the feedback and insight, I guess I'll need to keep digging deeper into some alternative solutions (language perhaps). Interesting enough, I'm also on macOS and was able to record microphone input with zero prompts. However the little yellow dot did turn on in my action bar. – Chris Feb 15 '22 at 09:27
  • Note: macOS authorisations are remembered. Look at the Microphone section of the Privacy page in the "Security & Privacy" System Preference. – greg-449 Feb 15 '22 at 10:22
  • FWIW, over the last decade I've seen this question asked multiple times (intercepting audio data stream in between some program and the speakers). Not once has there been an answer posted that shows how to do this or that it is even possible. You can explore the lines and ports that are exposed using API discussed here https://docs.oracle.com/javase/tutorial/sound/accessing.html. TargetDataLines read a data line, SourceDataLines write a data line. The naming is kind of backwards. – Phil Freihofner Feb 15 '22 at 17:15

0 Answers0