Juce ALSA just finds hardware devices?

13 posts / 0 new
Last post
Bruce Wheaton
Offline
Last seen: 6 hours 19 min ago
Joined: 17 Aug 2006 - 01:43
Juce ALSA just finds hardware devices?

Is there a good reason why juce only deals with direct ALSA hardware devices? There's a range of other ALSA features which using combining and piping which would be useful, especially since juce isn't 100% setup for multiple audio callbacks.

Bruce

jpo
Offline
Last seen: 4 hours 37 min ago
Joined: 20 Mar 2008 - 13:45
Re: Juce ALSA just finds hardware devices?

it would also provide compatibility with pulseaudio

jules
Offline
Last seen: 7 hours 5 min ago
Joined: 29 Apr 2013 - 18:37
Re: Juce ALSA just finds hardware devices?

I didn't explicitly make it only use hardware devices... Is there some kind of trick involved in finding/opening non-hardware ones?

jpo
Offline
Last seen: 4 hours 37 min ago
Joined: 20 Mar 2008 - 13:45
Re: Juce ALSA just finds hardware devices?

As far as I recall the code looks only for "hw" devices, the other "soft" devices are named "plughw" (? I'm not even sure of that). But there are other things to change in order to get it to work, I had a look at it some time ago and did not succeed.. However I don't know anything about alsa so the changes may be obvious to someone a bit more knowledgable

The main issue right now, is that when pulseaudio is running (which is very common nowadays), the corresponding hardware audio device in not available for juce. And the other way around, when a juce app is using the hardware audio device, pulseaudio cannot get access to it, so you have to use stuff like "pasuspender MyJuceApp" to temporarily disable pulseaudio while your app is running etc. This is also what JACK is using because getting exclusive access to the soundcard is crucial for the very best low latency , but being able to use also ALSA soft devices, such as the ones provided by pulseaudio, would be a nice thing to have (plus the features mentionned by Bruce)

Bruce Wheaton
Offline
Last seen: 6 hours 19 min ago
Joined: 17 Aug 2006 - 01:43
Re: Juce ALSA just finds hardware devices?

I'm unable to find why juce doesn't see the other devices so far. Maybe it is just the names. I'll keep digging.

Bruce

darryl
Offline
Last seen: 2 years 4 months ago
Joined: 23 Dec 2011 - 08:11
Re: Juce ALSA just finds hardware devices?

Has anyone found a work around for this?

I'd really like to use the ALSA dmix plugin, which provides for direct mixing of multiple streams for sound cards that don't support hardware mixing. Otherwise when I run my juce app other apps can't access the sound card, and if another app is running then my juce app can't access the sound card.

Bruce Wheaton
Offline
Last seen: 6 hours 19 min ago
Joined: 17 Aug 2006 - 01:43
Re: Juce ALSA just finds hardware devices?

I've had a quick dig - it's on my list.

My current suspicion is that the juce jack code it too keen to find devices starting with 'hw', and this disallows custom made devices. I can't find any function calls that don't look the same as everyone else's.

Bruce

jpo
Offline
Last seen: 4 hours 37 min ago
Joined: 20 Mar 2008 - 13:45
Re: Juce ALSA just finds hardware devices?

I've been working on the ALSA stuff recently, but I'm still very hesitating about the choices that should be made. Unfortunately it is very hard to obtain a list of output devices from alsa that is nice, easy to read, and not full of crap. There are indeed two lists that are provided by ALSA:

- the list of hardware devices , this is what JUCE currently uses . This is the list returned by aplay -l , except that juce does not list the devices that are already opened (by pulseaudio, typically..)
- the list of output devices, that may be hardware devices, or any of the many plugins of alsa (pulseaudio plugin, dmix, whatever). This is the list returned by aplay -L . This is the list that is officially recommended because the user may configure it in his ~/.asoundrc (this is not easy, though...). Do not expect this list to be same when you switch between distributions, there are as many variations of this list as there are linux distributions.
And unfortunately this list typically contains many entries that I find irrelevant or redundant (I may be wrong) , for examples all the surround40, surround41, surround50 etc entries that appear everytime. I also find these not hard to read, the device "names" are made of very long strings that are contain a line separator.. , for example:

Quote:
dmix:CARD=Intel,DEV=0
HDA Intel, ALC269 Analog
Direct sample mixing device
dmix:CARD=Intel,DEV=1
HDA Intel, ALC269 Digital
Direct sample mixing device
dmix:CARD=Intel,DEV=3
HDA Intel, HDMI 0
Direct sample mixing device
dsnoop:CARD=Intel,DEV=0
HDA Intel, ALC269 Analog
Direct sample snooping device
dsnoop:CARD=Intel,DEV=1
HDA Intel, ALC269 Digital
Direct sample snooping device
dsnoop:CARD=Intel,DEV=3
HDA Intel, HDMI 0
Direct sample snooping device
hw:CARD=Intel,DEV=0
HDA Intel, ALC269 Analog
Direct hardware device without any conversions
hw:CARD=Intel,DEV=1
HDA Intel, ALC269 Digital
Direct hardware device without any conversions
hw:CARD=Intel,DEV=3
HDA Intel, HDMI 0
Direct hardware device without any conversions
plughw:CARD=Intel,DEV=0
HDA Intel, ALC269 Analog
Hardware device with all software conversions
plughw:CARD=Intel,DEV=1
HDA Intel, ALC269 Digital
Hardware device with all software conversions
plughw:CARD=Intel,DEV=3
HDA Intel, HDMI 0
Hardware device with all software conversions

So I see four options:

(1) keep juce as it is now. That means that juce applications cannot use the soundcard when pulseaudio is using it. For "pro audio" apps, that is quite ok, for other apps that is a big issue.
(2) keep the hardware device list we have now, but add the alsa "default" output device, and maybe the 'pulse' output device , and make 'default' the default juce device. That won't satisfy the users that use dmix or advanced alsa configurations, but they are quite rare I think.
(3) provide only the list of output devices from aplay -L. This is the official recommendation from alsa and pulseaudio. Maybe with some filtering on devices that are useless (plughw, sysdefault, dsnoop for output devices, dmix for input devices...)
(4) provide both, maybe with two different AudioDeviceTypes , for example "ALSA" for the list of output devices, and "ALSA HW" for the list of hardware devices.

For now I have implemented option (4).

So if anyone has opinions on this matter, feel free to chime in ! I'm no alsa expert btw, so I may be wrong or misunderstanding some things.

Bruce Wheaton
Offline
Last seen: 6 hours 19 min ago
Joined: 17 Aug 2006 - 01:43
Re: Juce ALSA just finds hardware devices?

I think we would like option 3.

Can you post what code you changed to get the fuller list?

Bruce

jpo
Offline
Last seen: 4 hours 37 min ago
Joined: 20 Mar 2008 - 13:45
Re: Juce ALSA just finds hardware devices?

There is a version with a minimal number of changes here: http://www.rawmaterialsoftware.com/viewtopic.php?f=5&t=9229#p52522 , basically there were 3 things to change to allow it to work with non-hardware alsa devices:

- enumerate those devices with snd_device_name_hint
- fix getDeviceProperties by removing some calls to snd_ctl_open that where useless and failed on non-hardware devices
- swap the calls to snd_pcm_hw_params_set_access/isInterleaved in setParameters -- I have been a bit lazy and did not try to understand why it did not work in the 'non-interleaved' mode

Here is my current version, there are still a lot of debug statements , and it has more changes -- it is also more robust when the pulseaudio stream is suspended:

/*
  ==============================================================================

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-11 by Raw Material Software Ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the GNU General
   Public License (Version 2), as published by the Free Software Foundation.
   A copy of the license is included in the JUCE distribution, or can be found
   online at www.gnu.org/licenses.

   JUCE 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.

  ------------------------------------------------------------------------------

   To release a closed-source product which uses JUCE, commercial licenses are
   available: visit www.rawmaterialsoftware.com/juce for more information.

  ==============================================================================
*/
#include <iostream>
using std::cerr;
namespace
{
    int alsa_verbose = 1;

#define IF_ALSA_VERBOSE(x) if (alsa_verbose) std::cerr << x << "\n";
#define CHECKED_ALSA(x) (checked_alsa(x, #x, __LINE__, __PRETTY_FUNCTION__))

    int checked_alsa(int err, const char *what, int lnum, const char *fname) 
    {
        if (err < 0 && alsa_verbose) 
        {
            std::cerr << fname << ":" << lnum << " ALSA called failed: " << what << "; returned " << err << " (" << snd_strerror(err) << ")\n";
        }
        return err;
    }

    void getDeviceSampleRates (snd_pcm_t* handle, Array <int>& rates)
    {
        const int ratesToTry[] = { 22050, 32000, 44100, 48000, 88200, 96000, 176400, 192000, 0 };

        snd_pcm_hw_params_t* hwParams;
        snd_pcm_hw_params_alloca (&hwParams);

        //IF_ALSA_VERBOSE("getDeviceSampleRates:");
        for (int i = 0; ratesToTry[i] != 0; ++i)
        {
            if (snd_pcm_hw_params_any (handle, hwParams) >= 0
                 && snd_pcm_hw_params_test_rate (handle, hwParams, ratesToTry[i], 0) == 0)
            {
                rates.addIfNotAlreadyThere (ratesToTry[i]);
                //IF_ALSA_VERBOSE("  sample rate " << ratesToTry[i] << " is OK");
            }
        }
    }

    void getDeviceNumChannels (snd_pcm_t* handle, unsigned int* minChans, unsigned int* maxChans)
    {
        snd_pcm_hw_params_t *params;
        snd_pcm_hw_params_alloca (&params);

        if (snd_pcm_hw_params_any (handle, params) >= 0)
        {
            snd_pcm_hw_params_get_channels_min (params, minChans);
            snd_pcm_hw_params_get_channels_max (params, maxChans);
            IF_ALSA_VERBOSE("getDeviceNumChannels: " << *minChans << " " << *maxChans);
            *maxChans = jmin(*maxChans, 32u);
            *minChans = jmin(*minChans, *maxChans);
        } else IF_ALSA_VERBOSE("getDeviceNumChannels failed");
    }

    void getDeviceProperties (const String& deviceID,
                              unsigned int& minChansOut,
                              unsigned int& maxChansOut,
                              unsigned int& minChansIn,
                              unsigned int& maxChansIn,
                              Array <int>& rates, 
                              bool testOutput=true, bool testInput=true)
    {
        minChansOut = maxChansOut = minChansIn = maxChansIn = 0;
        int alsaCardIndex, alsaDeviceIndex;
        alsaCardIndex = alsaDeviceIndex = -1;

        if (deviceID.isEmpty())
            return;

        IF_ALSA_VERBOSE("getDeviceProperties(" << deviceID.toUTF8().getAddress() << ")");

        
        snd_pcm_info_t* info;
        snd_pcm_info_alloca (&info);
              
        if (testOutput) {
            snd_pcm_t* pcmHandle;
            if (CHECKED_ALSA(snd_pcm_open (&pcmHandle, deviceID.toUTF8().getAddress(), SND_PCM_STREAM_PLAYBACK, /*SND_PCM_ASYNC | */SND_PCM_NONBLOCK)) >= 0)
            {
                if (snd_pcm_info(pcmHandle, info) == 0) 
                {
                    snd_pcm_class_t cl = snd_pcm_info_get_class(info);
                    snd_pcm_subclass_t subcl = snd_pcm_info_get_subclass(info);
                    cerr << "deviceID:" << deviceID.toUTF8().getAddress() << ", REAL ID: " << snd_pcm_info_get_id(info); 
                    cerr << " class: " << cl << " subclass: " << subcl << ", REALNAM: " << snd_pcm_info_get_name(info) 
                         << ", CARD: " << snd_pcm_info_get_card(info) << ", DEV:" << snd_pcm_info_get_device(info) 
                         << ", SUBDEV: " << snd_pcm_info_get_subdevice(info) << "/" << snd_pcm_info_get_subdevices_count(info) 
                         << ", PCM TYPE: " << snd_pcm_type_name(snd_pcm_type(pcmHandle)) << "\n";
                    alsaCardIndex   = snd_pcm_info_get_card(info);
                    alsaDeviceIndex = snd_pcm_info_get_device(info);
                
                }
              
              
                getDeviceNumChannels (pcmHandle, &minChansOut, &maxChansOut);
                getDeviceSampleRates (pcmHandle, rates);
                
                snd_pcm_close (pcmHandle);
            } else cerr << "snd_pcm_open(" << deviceID.toUTF8().getAddress() << ", SND_PCM_STREAM_PLAYBACK) FAILED)\n";
        }
        if (testInput) {
            snd_pcm_t* pcmHandle;
            fflush(stdout); fflush(stderr);
            if (snd_pcm_open (&pcmHandle, deviceID.toUTF8(), SND_PCM_STREAM_CAPTURE, /*SND_PCM_ASYNC | */SND_PCM_NONBLOCK) >= 0)
            {
                if (snd_pcm_info(pcmHandle, info) == 0) 
                {
                    alsaCardIndex   = snd_pcm_info_get_card(info);
                    alsaDeviceIndex = snd_pcm_info_get_device(info);
                }
                fflush(stdout); fflush(stderr);
                getDeviceNumChannels (pcmHandle, &minChansIn, &maxChansIn);
                
                if (rates.size() == 0)
                    getDeviceSampleRates (pcmHandle, rates);
                
                snd_pcm_close (pcmHandle);
            }
            fflush(stdout); fflush(stderr);
        }
    }


//==============================================================================
class ALSADevice
{
  String devid;
public:
#define failed(x) failed_(x, #x, __LINE__)
    ALSADevice (const String& deviceID, bool forInput)
        : handle (0),
          bitDepth (16),
          numChannelsRunning (0),
          latency (0),
          isInput (forInput),
          isInterleaved (true)
    {
        devid = deviceID;
        IF_ALSA_VERBOSE("\nALSADevice::ALSADevice, calling snd_pcm_open(" << deviceID.toUTF8().getAddress() << ", forInput=" << forInput << ")");
        int err = snd_pcm_open (&handle, deviceID.toUTF8(),
                                forInput ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK,
                                SND_PCM_ASYNC);
        if (err < 0) { 
            error << "Could not open " << (forInput ? "input" : "output") << " device " << deviceID << ": " << snd_strerror(err);
            IF_ALSA_VERBOSE("snd_pcm_open failed; " << error);
        }
    }

    ~ALSADevice()
    {
      IF_ALSA_VERBOSE("ALSADevice::~ALSADevice , Closing device " << devid.toUTF8().getAddress());
      if (handle != 0) 
      {
          snd_pcm_close(handle);
      }
    }

    void closeNow() { if (handle) { snd_pcm_close(handle); handle = 0; } }

    bool setParameters (unsigned int sampleRate, int numChannels, int bufferSize)
    {
        if (handle == 0)
            return false;

        IF_ALSA_VERBOSE("ALSADevice::setParameters(" << devid.toUTF8().getAddress() << ", " << sampleRate << ", " << numChannels << ", " << bufferSize << ")");

        snd_pcm_hw_params_t* hwParams;
        snd_pcm_hw_params_alloca (&hwParams);

        if (failed (snd_pcm_hw_params_any (handle, hwParams)))
            return false;

        if (snd_pcm_hw_params_set_access (handle, hwParams, SND_PCM_ACCESS_RW_INTERLEAVED) >= 0) // works better for plughw..
            isInterleaved = true;
        else
          if (snd_pcm_hw_params_set_access (handle, hwParams, SND_PCM_ACCESS_RW_NONINTERLEAVED) >= 0)
            isInterleaved = false;
        else 
        {
            jassertfalse;
            return false;
        }

        enum { isFloatBit = 1 << 16, isLittleEndianBit = 1 << 17 };

        const int formatsToTry[] = { SND_PCM_FORMAT_FLOAT_LE,   32 | isFloatBit | isLittleEndianBit,
                                     SND_PCM_FORMAT_FLOAT_BE,   32 | isFloatBit,
                                     SND_PCM_FORMAT_S32_LE,     32 | isLittleEndianBit,
                                     SND_PCM_FORMAT_S32_BE,     32,
                                     SND_PCM_FORMAT_S24_3LE,    24 | isLittleEndianBit,
                                     SND_PCM_FORMAT_S24_3BE,    24,
                                     SND_PCM_FORMAT_S16_LE,     16 | isLittleEndianBit,
                                     SND_PCM_FORMAT_S16_BE,     16 };
        bitDepth = 0;

        for (int i = 0; i < numElementsInArray (formatsToTry); i += 2)
        {
            if (snd_pcm_hw_params_set_format (handle, hwParams, (_snd_pcm_format) formatsToTry [i]) >= 0)
            {
                bitDepth = formatsToTry [i + 1] & 255;
                const bool isFloat = (formatsToTry [i + 1] & isFloatBit) != 0;
                const bool isLittleEndian = (formatsToTry [i + 1] & isLittleEndianBit) != 0;
                converter = createConverter (isInput, bitDepth, isFloat, isLittleEndian, numChannels);

                IF_ALSA_VERBOSE("  ALSA format: bitDepth=" << bitDepth << ", isFloat=" << isFloat << ", isLittleEndian=" << isLittleEndian << ", numChannels=" << numChannels);
                break;
            }
        }

        if (bitDepth == 0)
        {
            error = "device doesn't support a compatible PCM format";
            DBG ("ALSA error: " + error + "\n");
            return false;
        }

        int dir = 0;
        unsigned int periods = 4;
        snd_pcm_uframes_t samplesPerPeriod = bufferSize;

        if (failed (snd_pcm_hw_params_set_rate_near (handle, hwParams, &sampleRate, 0))
            || failed (snd_pcm_hw_params_set_channels (handle, hwParams, numChannels))
            || failed (snd_pcm_hw_params_set_periods_near (handle, hwParams, &periods, &dir))
            || failed (snd_pcm_hw_params_set_period_size_near (handle, hwParams, &samplesPerPeriod, &dir))
            || failed (snd_pcm_hw_params (handle, hwParams)))
        {
            return false;
        }

        snd_pcm_uframes_t frames = 0;

        if (failed (snd_pcm_hw_params_get_period_size (hwParams, &frames, &dir))
             || failed (snd_pcm_hw_params_get_periods (hwParams, &periods, &dir)))
            latency = 0;
        else
            latency = frames * (periods - 1); // (this is the method JACK uses to guess the latency..)

        snd_pcm_sw_params_t* swParams;
        snd_pcm_sw_params_alloca (&swParams);
        snd_pcm_uframes_t boundary;

        if (failed (snd_pcm_sw_params_current (handle, swParams))
            || failed (snd_pcm_sw_params_get_boundary (swParams, &boundary))
            || failed (snd_pcm_sw_params_set_silence_threshold (handle, swParams, 0))
            || failed (snd_pcm_sw_params_set_silence_size (handle, swParams, boundary))
            || failed (snd_pcm_sw_params_set_start_threshold (handle, swParams, samplesPerPeriod))
            || failed (snd_pcm_sw_params_set_stop_threshold (handle, swParams, boundary))
            || failed (snd_pcm_sw_params (handle, swParams)))
        {
            return false;
        }

        if (alsa_verbose) {
          // enable this to dump the config of the devices that get opened
          snd_output_t* out;
          snd_output_stdio_attach (&out, stderr, 0);
          snd_pcm_hw_params_dump (hwParams, out);
          snd_pcm_sw_params_dump (swParams, out);
        }

        numChannelsRunning = numChannels;
        IF_ALSA_VERBOSE(" end of setParameters, numChannelsRunning=" << numChannelsRunning);
        return true;
    }

    //==============================================================================
    bool writeToOutputDevice (AudioSampleBuffer& outputChannelBuffer, const int numSamples)
    {
        jassert (numChannelsRunning <= outputChannelBuffer.getNumChannels());
        float** const data = outputChannelBuffer.getArrayOfChannels();
        snd_pcm_sframes_t numDone = 0;

        if (isInterleaved)
        {
            scratch.ensureSize (sizeof (float) * numSamples * numChannelsRunning, false);

            for (int i = 0; i < numChannelsRunning; ++i)
                converter->convertSamples (scratch.getData(), i, data[i], 0, numSamples);

            numDone = snd_pcm_writei (handle, scratch.getData(), numSamples);
        }
        else
        {
            for (int i = 0; i < numChannelsRunning; ++i)
                converter->convertSamples (data[i], data[i], numSamples);

            numDone = snd_pcm_writen (handle, (void**) data, numSamples);
        }

        if (failed (numDone))
        {
            if (numDone == -EPIPE)
            {
                if (failed (snd_pcm_prepare (handle)))
                    return false;
            }
            else if (numDone != -ESTRPIPE)
                return false;
        }

        return true;
    }

    bool readFromInputDevice (AudioSampleBuffer& inputChannelBuffer, const int numSamples)
    {
        jassert (numChannelsRunning <= inputChannelBuffer.getNumChannels());
        float** const data = inputChannelBuffer.getArrayOfChannels();

        if (isInterleaved)
        {
            scratch.ensureSize (sizeof (float) * numSamples * numChannelsRunning, false);
            scratch.fillWith (0); // (not clearing this data causes warnings in valgrind)

            snd_pcm_sframes_t num = snd_pcm_readi (handle, scratch.getData(), numSamples);

            if (failed (num))
            {
                if (num == -EPIPE)
                {
                    if (failed (snd_pcm_prepare (handle)))
                        return false;
                }
                else if (num != -ESTRPIPE)
                    return false;
            }

            for (int i = 0; i < numChannelsRunning; ++i)
                converter->convertSamples (data[i], 0, scratch.getData(), i, numSamples);
        }
        else
        {
            snd_pcm_sframes_t num = snd_pcm_readn (handle, (void**) data, numSamples);

            if (failed (num) && num != -EPIPE && num != -ESTRPIPE)
                return false;

            for (int i = 0; i < numChannelsRunning; ++i)
                converter->convertSamples (data[i], data[i], numSamples);
        }

        return true;
    }

    //==============================================================================
    snd_pcm_t* handle;
    String error;
    int bitDepth, numChannelsRunning, latency;

    //==============================================================================
private:
    const bool isInput;
    bool isInterleaved;
    MemoryBlock scratch;
    ScopedPointer<AudioData::Converter> converter;

    //==============================================================================
    template <class SampleType>
    struct ConverterHelper
    {
        static AudioData::Converter* createConverter (const bool forInput, const bool isLittleEndian, const int numInterleavedChannels)
        {
            if (forInput)
            {
                typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::NonConst> DestType;

                if (isLittleEndian)
                    return new AudioData::ConverterInstance <AudioData::Pointer <SampleType, AudioData::LittleEndian, AudioData::Interleaved, AudioData::Const>, DestType> (numInterleavedChannels, 1);
                else
                    return new AudioData::ConverterInstance <AudioData::Pointer <SampleType, AudioData::BigEndian, AudioData::Interleaved, AudioData::Const>, DestType> (numInterleavedChannels, 1);
            }
            else
            {
                typedef AudioData::Pointer <AudioData::Float32, AudioData::NativeEndian, AudioData::NonInterleaved, AudioData::Const> SourceType;

                if (isLittleEndian)
                    return new AudioData::ConverterInstance <SourceType, AudioData::Pointer <SampleType, AudioData::LittleEndian, AudioData::Interleaved, AudioData::NonConst> > (1, numInterleavedChannels);
                else
                    return new AudioData::ConverterInstance <SourceType, AudioData::Pointer <SampleType, AudioData::BigEndian, AudioData::Interleaved, AudioData::NonConst> > (1, numInterleavedChannels);
            }
        }
    };

    static AudioData::Converter* createConverter (const bool forInput, const int bitDepth, const bool isFloat, const bool isLittleEndian, const int numInterleavedChannels)
    {
        switch (bitDepth)
        {
            case 16:    return ConverterHelper <AudioData::Int16>::createConverter (forInput, isLittleEndian,  numInterleavedChannels);
            case 24:    return ConverterHelper <AudioData::Int24>::createConverter (forInput, isLittleEndian,  numInterleavedChannels);
            case 32:    return isFloat ? ConverterHelper <AudioData::Float32>::createConverter (forInput, isLittleEndian,  numInterleavedChannels)
                                       : ConverterHelper <AudioData::Int32>::createConverter (forInput, isLittleEndian,  numInterleavedChannels);
            default:    jassertfalse; break; // unsupported format!
        }

        return nullptr;
    }

    //==============================================================================

  bool failed_ (const int errorNum, const char *what, int lnum)
    {
        if (errorNum >= 0)
            return false;
        error = snd_strerror (errorNum);
        DBG ("ALSA error: " + error + "\n");
        IF_ALSA_VERBOSE("ALSA error " << errorNum << ", " << what << ":" << lnum);
        return true;
    }
#undef failed

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSADevice);
};

//==============================================================================
class ALSAThread  : public Thread
{
public:
    ALSAThread (const String& inputId_,
                const String& outputId_)
        : Thread ("Juce ALSA"),
          sampleRate (0),
          bufferSize (0),
          outputLatency (0),
          inputLatency (0),
          callback (0),
          inputId (inputId_),
          outputId (outputId_),
          numCallbacks (0),
          audioIoInProgress(false),
          inputChannelBuffer (1, 1),
          outputChannelBuffer (1, 1)
    {
        initialiseRatesAndChannels();
    }

    ~ALSAThread()
    {
        close();
    }

    void open (BigInteger inputChannels,
               BigInteger outputChannels,
               const double sampleRate_,
               const int bufferSize_)
    {
        close();

        error = String::empty;
        sampleRate = sampleRate_;
        bufferSize = bufferSize_;

        inputChannelBuffer.setSize (jmax ((int) minChansIn, inputChannels.getHighestBit()) + 1, bufferSize);
        inputChannelBuffer.clear();
        inputChannelDataForCallback.clear();
        currentInputChans.clear();

        if (inputChannels.getHighestBit() >= 0)
        {
            for (int i = 0; i <= jmax (inputChannels.getHighestBit(), (int) minChansIn); ++i)
            {
                if (inputChannels[i])
                {
                    inputChannelDataForCallback.add (inputChannelBuffer.getSampleData (i));
                    currentInputChans.setBit (i);
                }
            }
        }

        outputChannelBuffer.setSize (jmax ((int) minChansOut, outputChannels.getHighestBit()) + 1, bufferSize);
        outputChannelBuffer.clear();
        outputChannelDataForCallback.clear();
        currentOutputChans.clear();

        if (outputChannels.getHighestBit() >= 0)
        {
            for (int i = 0; i <= jmax (outputChannels.getHighestBit(), (int) minChansOut); ++i)
            {
                if (outputChannels[i])
                {
                    outputChannelDataForCallback.add (outputChannelBuffer.getSampleData (i));
                    currentOutputChans.setBit (i);
                }
            }
        }

        if (outputChannelDataForCallback.size() > 0 && outputId.isNotEmpty())
        {
            outputDevice = new ALSADevice (outputId, false);

            if (outputDevice->error.isNotEmpty())
            {
                error = outputDevice->error;
                outputDevice = nullptr;
                return;
            }

            currentOutputChans.setRange (0, minChansOut, true);

            if (! outputDevice->setParameters ((unsigned int) sampleRate,
                                               jlimit ((int) minChansOut, (int) maxChansOut, currentOutputChans.getHighestBit() + 1),
                                               bufferSize))
            {
                error = outputDevice->error;
                outputDevice = nullptr;
                return;
            }

            outputLatency = outputDevice->latency;
        }

        if (inputChannelDataForCallback.size() > 0 && inputId.isNotEmpty())
        {
            inputDevice = new ALSADevice (inputId, true);

            if (inputDevice->error.isNotEmpty())
            {
                error = inputDevice->error;
                inputDevice = nullptr;
                return;
            }

            currentInputChans.setRange (0, minChansIn, true);

            if (! inputDevice->setParameters ((unsigned int) sampleRate,
                                              jlimit ((int) minChansIn, (int) maxChansIn, currentInputChans.getHighestBit() + 1),
                                              bufferSize))
            {
                error = inputDevice->error;
                inputDevice = nullptr;
                return;
            }

            inputLatency = inputDevice->latency;
        }

        if (outputDevice == nullptr && inputDevice == nullptr)
        {
            error = "no channels";
            return;
        }

        if (outputDevice != nullptr && inputDevice != nullptr)
        {
            snd_pcm_link (outputDevice->handle, inputDevice->handle);
        }

        if (inputDevice != nullptr && failed (snd_pcm_prepare (inputDevice->handle)))
            return;

        if (outputDevice != nullptr && failed (snd_pcm_prepare (outputDevice->handle)))
            return;

        startThread (9);

        int count = 1000;

        while (numCallbacks == 0)
        {
            sleep (5);

            if (--count < 0 || ! isThreadRunning())
            {
                error = "device didn't start";
                break;
            }
        }
    }

    void close()
    {
        if (isThreadRunning()) {
            /* problem: when pulseaudio is suspended (with pasuspend) , the ALSAThread::run is just stuck in
               snd_pcm_writei -- no error, no nothing it just stays stuck. So the only way I found to exit "nicely"
               (that is without the "killing thread by force" of stopThread) , is to just call snd_pcm_close from
               here which will cause the thread to resume, and exit */
            signalThreadShouldExit();
            int numCallbacks0 = numCallbacks;
            bool exited = waitForThreadToExit(400);
            if (!exited && audioIoInProgress && numCallbacks == numCallbacks0) {
                IF_ALSA_VERBOSE("ALSA thread is stuck in a I/O.. Is pulseaudio suspended ? Now trying to wake it up a bit rudely");
                if (outputDevice) outputDevice->closeNow();
                if (inputDevice) inputDevice->closeNow();
            }
        }
        stopThread (6000);

        inputDevice = nullptr;
        outputDevice = nullptr;

        inputChannelBuffer.setSize (1, 1);
        outputChannelBuffer.setSize (1, 1);

        numCallbacks = 0;
    }

    void setCallback (AudioIODeviceCallback* const newCallback) noexcept
    {
        const ScopedLock sl (callbackLock);
        callback = newCallback;
    }

    void run()
    {
        while (! threadShouldExit())
        {
            if (inputDevice != nullptr && inputDevice->handle)
            {
                audioIoInProgress = true;
                if (! inputDevice->readFromInputDevice (inputChannelBuffer, bufferSize))
                {
                    DBG ("ALSA: read failure");
                    break;
                }
                audioIoInProgress = false;
            }

            if (threadShouldExit())
                break;

            {
                const ScopedLock sl (callbackLock);
                ++numCallbacks;

                if (callback != nullptr)
                {
                    callback->audioDeviceIOCallback ((const float**) inputChannelDataForCallback.getRawDataPointer(),
                                                     inputChannelDataForCallback.size(),
                                                     outputChannelDataForCallback.getRawDataPointer(),
                                                     outputChannelDataForCallback.size(),
                                                     bufferSize);
                }
                else
                {
                    for (int i = 0; i < outputChannelDataForCallback.size(); ++i)
                        zeromem (outputChannelDataForCallback[i], sizeof (float) * bufferSize);
                }
            }

            if (outputDevice != nullptr && outputDevice->handle)
            {
                failed (snd_pcm_wait (outputDevice->handle, 2000));

                if (threadShouldExit())
                    break;

                failed (snd_pcm_avail_update (outputDevice->handle));

                audioIoInProgress = true;
                if (! outputDevice->writeToOutputDevice (outputChannelBuffer, bufferSize))
                {
                    DBG ("ALSA: write failure");
                    break;
                }
                audioIoInProgress = false;
            }
        }
        audioIoInProgress = false;
    }

    int getBitDepth() const noexcept
    {
        if (outputDevice != nullptr)
            return outputDevice->bitDepth;

        if (inputDevice != nullptr)
            return inputDevice->bitDepth;

        return 16;
    }

    //==============================================================================
    String error;
    double sampleRate;
    int bufferSize, outputLatency, inputLatency;
    BigInteger currentInputChans, currentOutputChans;

    Array <int> sampleRates;
    StringArray channelNamesOut, channelNamesIn;
    AudioIODeviceCallback* callback;

private:
    //==============================================================================
    const String inputId, outputId;
    ScopedPointer<ALSADevice> outputDevice, inputDevice;
    int numCallbacks;
    bool audioIoInProgress;

    CriticalSection callbackLock;

    AudioSampleBuffer inputChannelBuffer, outputChannelBuffer;
    Array<float*> inputChannelDataForCallback, outputChannelDataForCallback;

    unsigned int minChansOut, maxChansOut;
    unsigned int minChansIn, maxChansIn;

    bool failed (const int errorNum)
    {
        if (errorNum >= 0)
            return false;

        error = snd_strerror (errorNum);
        DBG ("ALSA error: " + error + "\n");
        return true;
    }

    void initialiseRatesAndChannels()
    {
        sampleRates.clear();
        channelNamesOut.clear();
        channelNamesIn.clear();
        minChansOut = 0;
        maxChansOut = 0;
        minChansIn = 0;
        maxChansIn = 0;
        unsigned int dummy = 0;

        getDeviceProperties (inputId, dummy, dummy, minChansIn, maxChansIn, sampleRates, false, true);
        getDeviceProperties (outputId, minChansOut, maxChansOut, dummy, dummy, sampleRates, true, false);

        unsigned int i;
        for (i = 0; i < maxChansOut; ++i)
            channelNamesOut.add ("channel " + String ((int) i + 1));

        for (i = 0; i < maxChansIn; ++i)
            channelNamesIn.add ("channel " + String ((int) i + 1));
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSAThread);
};


//==============================================================================
class ALSAAudioIODevice   : public AudioIODevice
{
public:
    ALSAAudioIODevice (const String& deviceName,
                       const String& typeName,
                       const String& inputId_,
                       const String& outputId_)
        : AudioIODevice (deviceName, typeName),
          inputId (inputId_),
          outputId (outputId_),
          isOpen_ (false),
          isStarted (false),
          internal (inputId_, outputId_)
    {
    }

    ~ALSAAudioIODevice()
    {
        close();
    }

    StringArray getOutputChannelNames()             { return internal.channelNamesOut; }
    StringArray getInputChannelNames()              { return internal.channelNamesIn; }

    int getNumSampleRates()                         { return internal.sampleRates.size(); }
    double getSampleRate (int index)                { return internal.sampleRates 
    ; } int getDefaultBufferSize() { return 512; } int getNumBufferSizesAvailable() { return 50; } int getBufferSizeSamples (int index) { int n = 16; for (int i = 0; i < index; ++i) n += n < 64 ? 16 : (n < 512 ? 32 : (n < 1024 ? 64 : (n < 2048 ? 128 : 256))); return n; } String open (const BigInteger& inputChannels, const BigInteger& outputChannels, double sampleRate, int bufferSizeSamples) { close(); if (bufferSizeSamples <= 0) bufferSizeSamples = getDefaultBufferSize(); if (sampleRate <= 0) { for (int i = 0; i < getNumSampleRates(); ++i) { if (getSampleRate (i) >= 44100) { sampleRate = getSampleRate (i); break; } } } internal.open (inputChannels, outputChannels, sampleRate, bufferSizeSamples); isOpen_ = internal.error.isEmpty(); return internal.error; } void close() { stop(); internal.close(); isOpen_ = false; } bool isOpen() { return isOpen_; } bool isPlaying() { return isStarted && internal.error.isEmpty(); } String getLastError() { return internal.error; } int getCurrentBufferSizeSamples() { return internal.bufferSize; } double getCurrentSampleRate() { return internal.sampleRate; } int getCurrentBitDepth() { return internal.getBitDepth(); } BigInteger getActiveOutputChannels() const { return internal.currentOutputChans; } BigInteger getActiveInputChannels() const { return internal.currentInputChans; } int getOutputLatencyInSamples() { return internal.outputLatency; } int getInputLatencyInSamples() { return internal.inputLatency; } void start (AudioIODeviceCallback* callback) { if (! isOpen_) callback = nullptr; if (callback != nullptr) callback->audioDeviceAboutToStart (this); internal.setCallback (callback); isStarted = (callback != nullptr); } void stop() { AudioIODeviceCallback* const oldCallback = internal.callback; start (0); if (oldCallback != nullptr) oldCallback->audioDeviceStopped(); } String inputId, outputId; private: bool isOpen_, isStarted; ALSAThread internal; }; //============================================================================== class ALSAAudioIODeviceType : public AudioIODeviceType { static void alsaSilentErrorHandler(const char */*file*/, int /*line*/, const char */*function*/, int /*err*/, const char */*fmt*/,...) { } public: //============================================================================== ALSAAudioIODeviceType(bool listOnlySoundcards_, const String &typeName) : AudioIODeviceType (typeName), hasScanned (false), listOnlySoundcards(listOnlySoundcards_) { const char *v = getenv("ALSA_VERBOSE"); if (v && v[0] == '1') alsa_verbose = 1; if (!alsa_verbose) { snd_lib_error_set_handler (&ALSAAudioIODeviceType::alsaSilentErrorHandler); } } ~ALSAAudioIODeviceType() { if (alsa_verbose) { snd_lib_error_set_handler(0); } } //============================================================================== void scanForDevices() { if (hasScanned) return; hasScanned = true; inputNames.clear(); inputIds.clear(); outputNames.clear(); outputIds.clear(); IF_ALSA_VERBOSE("ALSAAudioIODeviceType::scanForDevices()"); int cardNum = -1; if (listOnlySoundcards) { enumerateAlsaSoundcards(); } else { enumerateAlsaPcmDevices(); } inputNames.appendNumbersToDuplicates (false, true); outputNames.appendNumbersToDuplicates (false, true); } StringArray getDeviceNames (bool wantInputNames) const { jassert (hasScanned); // need to call scanForDevices() before doing this return wantInputNames ? inputNames : outputNames; } int getDefaultDeviceIndex (bool forInput) const { int idx = 0; if (forInput) idx = inputIds.indexOf("default"); else idx = outputIds.indexOf("default"); jassert (hasScanned); // need to call scanForDevices() before doing this return idx >= 0 ? idx : 0; } bool hasSeparateInputsAndOutputs() const { return true; } int getIndexOfDevice (AudioIODevice* device, bool asInput) const { jassert (hasScanned); // need to call scanForDevices() before doing this ALSAAudioIODevice* d = dynamic_cast <ALSAAudioIODevice*> (device); if (d == nullptr) return -1; return asInput ? inputIds.indexOf (d->inputId) : outputIds.indexOf (d->outputId); } AudioIODevice* createDevice (const String& outputDeviceName, const String& inputDeviceName) { jassert (hasScanned); // need to call scanForDevices() before doing this const int inputIndex = inputNames.indexOf (inputDeviceName); const int outputIndex = outputNames.indexOf (outputDeviceName); String deviceName (outputIndex >= 0 ? outputDeviceName : inputDeviceName); if (inputIndex >= 0 || outputIndex >= 0) return new ALSAAudioIODevice (deviceName, getTypeName(), inputIds [inputIndex], outputIds [outputIndex]); return nullptr; } //============================================================================== private: StringArray inputNames, outputNames, inputIds, outputIds; bool hasScanned, listOnlySoundcards; bool testDevice(const String &id, const String &outputName, const String &inputName) { unsigned int minChansOut = 0, maxChansOut = 0; unsigned int minChansIn = 0, maxChansIn = 0; Array <int> rates; bool isInput = inputName.isNotEmpty(), isOutput = outputName.isNotEmpty(); getDeviceProperties (id, minChansOut, maxChansOut, minChansIn, maxChansIn, rates, isOutput, isInput); isInput = maxChansIn > 0; isOutput = maxChansOut > 0; if ((isInput || isOutput) && rates.size() > 0) { IF_ALSA_VERBOSE("Device: '" << id.toUTF8().getAddress() << "' -> isInput: " << isInput << ", isOutput: " << isOutput); if (isInput) { inputNames.add(inputName); inputIds.add(id); } if (isOutput) { outputNames.add(outputName); outputIds.add(id); } return (isInput || isOutput); } else return false; } void enumerateAlsaSoundcards() { snd_ctl_t* handle = nullptr; snd_ctl_card_info_t* info = nullptr; snd_ctl_card_info_alloca (&info); int cardNum = -1; while (/*!displayAllAlsaDevices && */outputIds.size() + inputIds.size() <= 32) { snd_card_next (&cardNum); if (cardNum < 0) break; if (CHECKED_ALSA(snd_ctl_open (&handle, ("hw:" + String (cardNum)).toUTF8(), SND_CTL_NONBLOCK)) >= 0) { if (CHECKED_ALSA(snd_ctl_card_info (handle, info)) >= 0) { String cardId (snd_ctl_card_info_get_id (info)); if (cardId.removeCharacters ("0123456789").isEmpty()) cardId = String (cardNum); String cardName = snd_ctl_card_info_get_name (info); if (cardName.isEmpty()) cardName = cardId; int device = -1; /*cerr << "HW cardId=" << cardId.toUTF8().getAddress() << " -- driver:" << snd_ctl_card_info_get_driver (info) << " -- mixer: " << snd_ctl_card_info_get_mixername (info) << " -- components: " << snd_ctl_card_info_get_components(info) << "\n";*/ snd_pcm_info_t *pcmInfo; snd_pcm_info_alloca( &pcmInfo ); for (;;) { if (snd_ctl_pcm_next_device (handle, &device) < 0 || device < 0) break; snd_pcm_info_set_device (pcmInfo, device); for (int subDevice=0, nbSubDevice=1; subDevice < nbSubDevice; ++subDevice) { snd_pcm_info_set_subdevice (pcmInfo, subDevice); snd_pcm_info_set_stream (pcmInfo, SND_PCM_STREAM_CAPTURE); bool isInput = (snd_ctl_pcm_info (handle, pcmInfo) >= 0); snd_pcm_info_set_stream (pcmInfo, SND_PCM_STREAM_PLAYBACK); bool isOutput = (snd_ctl_pcm_info (handle, pcmInfo) >= 0); if (!isInput && !isOutput) continue; if (nbSubDevice == 1) { nbSubDevice = snd_pcm_info_get_subdevices_count (pcmInfo); } String id, name; if (nbSubDevice == 1) { id << "hw:" << cardId << "," << device; name << cardName << ", " << snd_pcm_info_get_name (pcmInfo); } else { id << "hw:" << cardId << "," << device << "," << subDevice; name << cardName << ", " << snd_pcm_info_get_name (pcmInfo) << " {" << snd_pcm_info_get_subdevice_name (pcmInfo) << "}"; } IF_ALSA_VERBOSE("Soundcard ID: " << id << ", name: '" << name << ", isInput:" << isInput << ", isOutput:" << isOutput << "\n"); if (isInput) { inputNames.add(name); inputIds.add(id); } if (isOutput) { outputNames.add(name); outputIds.add(id); } } } } CHECKED_ALSA(snd_ctl_close (handle)); } } } void enumerateAlsaPcmDevices() { void **hints = 0; if (CHECKED_ALSA(snd_device_name_hint(-1, "pcm", &hints)) == 0) { for (char **h = (char**)hints; *h; ++h) { String id, description, ioid; { char *aid = 0, *adesc = 0, *aioid = 0; aid = snd_device_name_get_hint(*h, "NAME"); adesc = snd_device_name_get_hint(*h, "DESC"); aioid = snd_device_name_get_hint(*h, "IOID"); // NULL, or Input or Output; NULL means Input+Output id << aid; description << adesc; ioid << aioid; free(aid); free(adesc); free(aioid); } IF_ALSA_VERBOSE("ID: " << id << "; desc: " << description << "; ioid: " << ioid); if (id.isEmpty() || id.startsWith("default:") || id.startsWith("sysdefault:") || id.startsWith("plughw:") || id == "null") continue; String name = String(description).replace("\n", "; "); if (name.isEmpty()) name = id; //if (id.startsWith("dmix")) { name += " [dmix]"; } /* we can already find out if the device is input-only, output-only or duplex this will avoid triggering spurious alsa warning when snd_pcm_open(SND_PCM_STREAM_PLAYBACK) a read-only pcm device */ bool isOutput = (ioid != "Input"); bool isInput = (ioid != "Output"); /* alsa is stupid here, it advertises dmix and dsnoop as input/output devices, and while opening dmix as input, or dsnoop as output will cause it to burp errors in the terminal.. */ isInput = isInput && !id.startsWith("dmix"); isOutput = isOutput && !id.startsWith("dsnoop"); testDevice(id, isOutput ? name : String::empty, isInput ? name : String::empty); } snd_device_name_free_hint(hints); } /* sometimes the "default" device is not listed, but it is nice to see it explicitely in the list */ if (!outputIds.contains("default")) { testDevice("default", "Default ALSA Output", "Default ALSA Input"); } /* same for the pulseaudio plugin */ if (!outputIds.contains("pulse")) { testDevice("pulse", "Pulseaudio output", "Pulseaudio input"); } /* make sure the default device is listed first, and followed by the pulse device (if present) */ int idx; idx = outputIds.indexOf("pulse"); moveToFront(outputIds, idx); moveToFront(outputNames, idx); idx = inputIds.indexOf("pulse"); moveToFront(inputIds, idx); moveToFront(inputNames, idx); idx = outputIds.indexOf("default"); moveToFront(outputIds, idx); moveToFront(outputNames, idx); idx = inputIds.indexOf("default"); moveToFront(inputIds, idx); moveToFront(inputNames, idx); } static void moveToFront(StringArray &a, int idx) { if (idx > 0) { String s = a[idx]; a.remove(idx); a.insert(0, s); } } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ALSAAudioIODeviceType); }; } //============================================================================== AudioIODeviceType* createAudioIODeviceType_ALSA_Soundcards() { return new ALSAAudioIODeviceType(true, "ALSA HW"); } AudioIODeviceType* createAudioIODeviceType_ALSA_PcmDevices() { return new ALSAAudioIODeviceType(false, "ALSA"); } AudioIODeviceType* AudioIODeviceType::createAudioIODeviceType_ALSA() { //return createAudioIODeviceType_ALSA_Soundcards(); return createAudioIODeviceType_ALSA_PcmDevices(); }

    There is still something that I consider an issue with the pcm devices list (enumerateAlsaPcmDevices above) : there will be missing entries in the list if it is built while pulseaudio is running , because getDeviceProperties tries to probe all the devices and some of them will report they are "busy" and will be skipped. This is also the case in juce git device list, but it is not the case in my 'enumerateAlsaSoundcards' function above with is not attempting to open the soundcards when enumerating them

    falkTX
    falkTX's picture
    Offline
    Last seen: 5 hours 20 min ago
    Joined: 4 Jun 2011 - 16:15
    Re: Juce ALSA just finds hardware devices?

    Option 3 seems better, but having a 'default' option will be nice too.
    I use a very customized asoundrc file that uses snd-aloop to output all ALSA sound directly to JACK (using alsa_in/out tools).
    Most ALSA applications I've used fail to properly see the virtual-loop devices and only work when using the 'default' option.

    For reference only, here's my asoundrc:

    # ------------------------------------------------------
    # Custom asoundrc file for use with snd-aloop and JACK
    
    # ------------------------------------------------------
    # playback device
    pcm.aloopPlayback {
      type dmix
      ipc_key 1
      ipc_key_add_uid true
      slave {
        pcm "hw:Loopback,0,0"
        format S32_LE
        rate {
          @func igetenv
          vars [ JACK_SAMPLE_RATE ]
          default 44100
        }
        period_size {
          @func igetenv
          vars [ JACK_PERIOD_SIZE ]
          default 1024
        }
        buffer_size 4096
      }
    }
    
    # capture device
    pcm.aloopCapture {
      type dsnoop
      ipc_key 2
      ipc_key_add_uid true
      slave {
        pcm "hw:Loopback,0,1"
        format S32_LE
        rate {
          @func igetenv
          vars [ JACK_SAMPLE_RATE ]
          default 44100
        }
        period_size {
          @func igetenv
          vars [ JACK_PERIOD_SIZE ]
          default 1024
        }
        buffer_size 4096
      }
    }
    
    # duplex device
    pcm.aloopDuplex {
      type asym
      playback.pcm "aloopPlayback"
      capture.pcm "aloopCapture"
    }
    
    # ------------------------------------------------------
    # default device
    
    pcm.!default {
      type plug
      slave.pcm "aloopDuplex"
    }
    
    # ------------------------------------------------------
    # alsa_in -j alsa_in -dcloop -q 1
    pcm.cloop {
      type dsnoop
      ipc_key 3
      ipc_key_add_uid true
      slave {
        pcm "hw:Loopback,1,0"
        format S32_LE
        rate {
          @func igetenv
          vars [ JACK_SAMPLE_RATE ]
          default 44100
        }
        period_size {
          @func igetenv
          vars [ JACK_PERIOD_SIZE ]
          default 1024
        }
        buffer_size 4096
      }
    }
    
    # ------------------------------------------------------
    # alsa_out -j alsa_out -dploop -q 1
    pcm.ploop {
      type plug
      slave {
        pcm "hw:Loopback,1,1"
      }
    }
    

    Pages