Menu

#116 Jukebox mode does not play 6 channel FLAC

4.9
open
nobody
None
1
2015-01-21
2014-04-12
Anonymous
No

Description
Subsonic does not play 6 channel FLAC files in Jukebox mode. I attached the first 10s of an example file.

I use Subsonic Version 4.9, Standalone (jetty-6.1.x, java 1.7.0_45) and Gentoo with Kernel 3.10.17 as OS. The default audio device is the HDMI output of an HDA Intel PCH. The audio subsystem is ALSA 1.0.27.2.

The file plays as intended with MPlayer 1.1-4.7.3 using

mplayer -ao alsa:device=hw=0.8 -channels 6 -format s16le sample.flac

subsonic.log

[2014-04-11 21:12:33,765] DEBUG TranscodeInputStream - Starting transcoder: [ffmpeg] [-ss] [0] [-i] [sample.flac] [-v] [0] [-f] [au] [-]
[2014-04-11 21:12:33,785] DEBUG JukeboxService - Opened line com.sun.media.sound.DirectAudioDevice$DirectSDL@4c492d3b
[2014-04-11 21:12:33,785] INFO JukeboxService - firefox starting jukebox for "sample.flac"
[2014-04-11 21:12:33,786] WARN JukeboxService - Error when copying audio data: java.lang.IllegalArgumentException: illegal request to write non-integral number of frames (8168 bytes, frameSize = 12 bytes)
java.lang.IllegalArgumentException: illegal request to write non-integral number of frames (8168 bytes, frameSize = 12 bytes)
        at com.sun.media.sound.DirectAudioDevice$DirectDL.write(DirectAudioDevice.java:731)
        at net.sourceforge.subsonic.service.jukebox.AudioPlayer$AudioDataWriter.run(AudioPlayer.java:184)
        at java.lang.Thread.run(Thread.java:744)

[2014-04-11 21:12:33,795] DEBUG JukeboxService - Closed line com.sun.media.sound.DirectAudioDevice$DirectSDL@4c492d3b

mediainfo sample.flac
General
Complete name : sample.flac
Format : FLAC
Format/Info : Free Lossless Audio Codec
File size : 65.8 MiB
Duration : 4mn 1s
Overall bit rate mode : Variable
Overall bit rate : 2 287 Kbps
Audio
Format : FLAC
Format/Info : Free Lossless Audio Codec
Duration : 4mn 1s
Bit rate mode : Variable
Bit rate : 2 287 Kbps
Channel count : 6 channels
Sampling rate : 48.0 KHz
Bit depth : 16 bits
Replay gain : 0.00 dB
Replay gain peak : 0.000000
Stream size : 65.8 MiB (100%)
Writing library : Lavf53.29.100
Language : English

ffmpeg -version
ffmpeg version 1.0.8
built on Dec 18 2013 09:46:33 with gcc 4.7.3 (Gentoo 4.7.3-r1 p1.3, pie-0.5.5)
configuration: --prefix=/usr --libdir=/usr/lib64 --shlibdir=/usr/lib64 --mandir=/usr/share/man --enable-shared --cc=x86_64-pc-linux-gnu-gcc --cxx=x86_64-pc-linux-gnu-g++ --ar=x86_64-pc-linux-gnu-ar --optflags='-march=native -O2 -pipe -fomit-frame-pointer' --extra-cflags='-march=native -O2 -pipe -fomit-frame-pointer' --extra-cxxflags='-march=native -O2 -pipe -fomit-frame-pointer' --disable-static --enable-gpl --enable-version3 --enable-postproc --enable-avfilter --enable-avresample --disable-stripping --disable-debug --disable-doc --disable-vaapi --disable-vdpau --disable-ffplay --disable-runtime-cpudetect --enable-libmp3lame --enable-libvo-aacenc --enable-libx264 --enable-libxvid --enable-libaacplus --enable-nonfree --disable-indev=v4l2 --disable-indev=alsa --disable-indev=oss --disable-indev=jack --disable-outdev=alsa --disable-outdev=oss --disable-outdev=sdl --enable-libfreetype --enable-pthreads --disable-amd3dnow --disable-amd3dnowext --disable-altivec --disable-mmxext --disable-vis --disable-neon --cpu=host --enable-hardcoded-tables
libavutil 51. 73.101 / 51. 73.101
libavcodec 54. 59.100 / 54. 59.100
libavformat 54. 29.104 / 54. 29.104
libavdevice 54. 2.101 / 54. 2.101
libavfilter 3. 17.100 / 3. 17.100
libswscale 2. 1.101 / 2. 1.101
libswresample 0. 15.100 / 0. 15.100
libpostproc 52. 0.100 / 52. 0.100

1 Attachments

Discussion

  • Anonymous

    Anonymous - 2014-04-12

    Addition: Jukebox mode plays other FLAC files and mp3s fine

     
  • Daniel

    Daniel - 2014-04-17

    Ok, so I fixed this problem for my system. It was fairly complicated as I don't have any experience with audio file formats and their conversion, and I fear that my fix is only applicable to my setup. The later is also the reason why I think this ticket should stay open until the problem is really addressed. Nevertheless, some components of it could be integrated into Subsonic as long as someone with knowledge about audio formats reviews them first.

    The actual fix consists of 4 parts.

    (1) System buffer size (ALSA)
    It seems that my system did not set the sound card buffer size large enough to handle all audio formats that the card supports.
    Trying to play multi-channel files with Subsonic always failed with the exception <core>javax.sound.sampled.LineUnavailableException</core>
    The same file with mplayer also yields this warning:

    % mplayer -ao alsa:device=hw=0.8 -channels 6 sample.flac
    [AO_ALSA] Unable to set buffer time near: Invalid argument
    Failed to initialize audio driver 'alsa:device=hw=0.8'
    

    Did some research, found http://mailman.alsa-project.org/pipermail/alsa-devel/2014-February/072265.html
    And sure enough, the buffer size was small:

    % cat /proc/asound/card0/pcm8p/sub0/prealloc 
    64
    

    Changing the buffer size to something higher makes the problem go away.

    % echo 1024 > /proc/asound/card0/pcm8p/sub0/prealloc
    

    (2) java.lang.IllegalArgumentException: illegal request to write non-integral number of frames
    The exception is thrown in net.sourceforge.subsonic.service.jukebox.AudioPlayer during line.write(buffer, 0, n) in line 184.

    The Javadoc for javax.sound.sampled.SourceDataLine states that

    The number of bytes to write must represent an integral number of sample frames, such that: [ bytes written ] % [frame size in bytes ] == 0

    This is no problem as long as the frame size is some power of 2. But for 6 channel audio files, the frame size is 12 (2 bytes per sample), which could lead to buffer not fulfilling the specification.
    I fixed this by adding the parameters int bytesPerFrame to AudioDataWriter and changing the PLAYING case of run() to

    case PLAYING:
        int n = in.read(buffer);
    
        if (n == -1) {
            setState(State.EOM);
            return;
        }
    
        // "integral number of bytes"
        // [ bytes written ] % [frame size in bytes ] == 0
        // This loop will fill the buffer from the stream s.t.
        // the spec holds.
        while (n % bytesPerFrame != 0) {
            int j = in.read(buffer, n, bytesPerFrame
                    - (n % bytesPerFrame));
            if (j == -1) {
                setState(State.EOM);
                n = n - (n % bytesPerFrame);
            } else {
                n = n + j;
            }
        }
        line.write(buffer, 0, n);
        break;
    

    (3) Hardware support
    After (1) and (2), Subsonic finally started playing multi-channel files. But after listening in, I noticed that the channels were in the wrong order. Also, one channel was always missing and instead, a typical, high-pitched "pfft" sound was audible. Strange enough, mplayer played the file correctly. But looking closer at the mplayer output, I noticed that it converted the file from s32be to s32le.

    I checked Java's understanding of the capabilities of my sound card (its a Intel Corporation 7 Series/C210 Series Chipset Family High Definition Audio Controller (rev 04)) and found that it thought it would support big endian encoding of PCM streams. mplayer differed there and it played the file just fine, so I assumed that Java does not report the right hardware capabilities.

    I tried to produce multi-channel files for Java that were (a) in s32le or s16le and (b) readable, but I failed. I got Java only to accept multi-channel sound streams in .au format (as produced by ffmpeg -f au). Neither wav nor aiff did work. As .au is strictly big endian encoded, I had to change the encoding in Java itself.

    I did this by changing the constructor of AudioPlayer to the following:

    public AudioPlayer(InputStream in, Listener listener) throws Exception {
        this.in = new BufferedInputStream(in);
        this.listener = listener;
    
        AudioFormat format = AudioSystem.getAudioFileFormat(this.in).getFormat();
    
        // try reencoding in PCM Signed little endian with same sample rate 
        // and size as input
        AudioFormat target = new AudioFormat(Encoding.PCM_SIGNED, 
            format.getSampleRate(), format.getSampleSizeInBits(),
            format.getChannels(), format.getFrameSize(),
            format.getFrameRate(), false);
    
        if (AudioSystem.isConversionSupported(target, format)) {
            AudioInputStream stream = AudioSystem.getAudioInputStream(target,
                    AudioSystem.getAudioInputStream(this.in));
            in = stream;
            line = AudioSystem.getSourceDataLine(target);
        } else {
            // if this fails, just use the old method 
            line = AudioSystem.getSourceDataLine(format);
        }
    
        line.open(format);
    
        if (line.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
            gainControl = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN);
            setGain(DEFAULT_GAIN);
        }
        new AudioDataWriter(format.getFrameSize(), format.getChannels(), order);
    }
    

    Sure enough, now all channels were present. But they were still in the wrong order.

    (4) Channel ordering
    As mplayer did play all the audio files on the right channels, I again assumed that Java did not map the sound hardware properly.

    After experimenting with the 8 channel sound test file from the Gentoo wiki ("Microsoft's 8 Channel Speaker Test"), I tried to remap the channels in Subsonic itself. Unfortunately, Java does not have any kind of support (or at least I could not find it), to show the target mapping or assist in the remapping. I therefore coded some primitive reordering function to reorder the bytes directly in the input stream:

    private static void reorderBufferContent(byte[] buffer, int offset,
                int length, int channelSize, int channelCount, int[] order) {
        int frameSize = channelSize * channelCount;
        byte[] tmp = new byte[frameSize];
    
        for (int i = offset; i < offset + length; i = i + frameSize) {
            // iterate over the buffer frame-wise (starting at offset)
            // buffer[i] ... buffer[i+framesize] is one frame
            for (int j = 0; j < channelCount; j++) {
                // iterate over each channel
                // j is position in original, order[j] is target
                for (int k = 0; k < channelSize; k++) {
                    // iterate over the individual bytes of the current channel
                    // j of the buffer
                    tmp[order[j] * channelSize + k] = buffer[i + j
                            * channelSize + k];
                }
            }
            // write back
            for (int j = 0; j < frameSize; j++) {
                buffer[i + j] = tmp[j];
            }
        }
    }
    

    The function assumes that a frame consists of successive channels with size channelSize in bytes and ordered from 0 to channelCount - 1. It uses the parameter order to map from the source position of a channel to a target position. My setup consists of the following 6 output channels:

    0 front-left
    1 front right
    2 back left (6)
    3 back right (7)
    4 center
    5 lfe
    

    As I tested an 8 channel file, side left and right were correctly mapped to back left and right, respectively. After some testing, I found that order = {0, 1, 4, 5, 2, 3, 6, 7} does the trick and brinsg my channels in the right order.

    Conclusion
    My problem is solved - for now. I think part 1 and 2 of my solution could help other people as-is. But part 3 and 4 seem to be too specific to my setup and may also dependent on the Java VM (I use dev-java/icedtea-bin-7.2.4.3).
    The problem in Subsonic as a whole remains.

    PS: I am the original reporter of this ticket (I just forgot to login after I pressed submit, so, apologies).
    PPS: I attached a diff to the current trunk version for reference.

     

Anonymous
Anonymous

Add attachments
Cancel