codenameone / CodenameOne

Cross-platform framework for building truly native mobile apps with Java or Kotlin. Write Once Run Anywhere support for iOS, Android, Desktop & Web.
https://www.codenameone.com/
Other
1.7k stars 405 forks source link

sound clips play unreliably on Android #3480

Open ddyer0 opened 3 years ago

ddyer0 commented 3 years ago

The attached test program plays a series of short sound clips, each clip plays 3 times, and should therefore be identical each time. They are not. Sometimes a clip plays correctly, sometimes not at all, sometimes in an audibly damaged way. This problem manifests differently on different hardware, and short clips seem to be particularly problematic. For comparison, you can build this for IOS.

The times reported while the clips are playing are elapsed time from start to when the doneplaying callback is executed. Its' suspicious that the play times are irregular and sometimes shorter than the sound clips actual duration.

package com.boardspace.dtest;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;

import com.codename1.media.MediaManager;
import com.codename1.ui.Component;
// issue #2567
// this version shows that g.rotate interacts badly with clipping
//
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.ui.Label;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.Resources;
import com.codename1.ui.Graphics;
import com.codename1.ui.layouts.BorderLayout;

class Clip 
{   InputStream stream;
    String name;
    long endtime = 0;
    long starttime = 0;
    Dtest parent;
    String error = "";
    Runnable notifier = new Runnable() 
        { public void run() 
            { endtime = System.currentTimeMillis();
              try { stream.close(); }
              catch (IOException e) {}
              synchronized(parent) { parent.notify(); }
            }
        };  
    Clip()
    {
    }
    public String toString()
    {   long et = endtime;
        if(et<=starttime) { et = System.currentTimeMillis(); }
        return("<clip "+name+" "+(et-starttime)+(error==null?"":error)+">");
    }
    public void play(Dtest p,String n,InputStream s)
    {
        parent = p;
        stream = s;
        name = n;
        starttime = System.currentTimeMillis();
        endtime = starttime-1;
        try {
        MediaManager.createMedia(stream, "audio/wav", notifier  ).play();
        }
        catch(IOException e)
        {
            error = e.toString();
        }
    }
}

public class Dtest 
{

    public String clips[] = {
                "badhint.wav",
                "click4.wav",               
                "pick-3.wav",
                "cannon.wav",
                "chat.wav",
                "Doorbl.wav",
                "chatchimes.wav",
                "gamechat.wav",
                "goodhint.wav",
                "droptile.wav",
                "dkbell.wav",
                "drop-1.wav",
                "drop-3.wav",
                "knock.wav",
                "cowbell.wav",
                "scrape.wav",
                "ticktock.wav", 
        };

private Form current;
@SuppressWarnings("unused")
private Resources theme;

public void init(Object context) {
    theme = UIManager.initFirstTheme("/theme");
    }

public int loops = 0;
public Clip finishedClip;
public String errorMessage;

public void playClip(Component hi,Resources res,String clip) throws IOException
{   int aa[]=null;
    InputStream stream = res.getData(clip);
    if(stream==null) 
      { errorMessage = "clip "+clip+" not found"; 
      }
    else {
        hi.repaint();
        Clip cl = new Clip();
        finishedClip = cl;
        cl.play(this,clip,stream);
        while(cl.endtime<cl.starttime)
        {
            try {
                Thread.sleep(10);
                hi.repaint();
            } catch (InterruptedException e) {
                errorMessage = e.toString();
            }
        }
        hi.repaint();
        }
        try {
            Thread.sleep(2000);
            } catch (InterruptedException e) {
        e.printStackTrace();
            }
}
public void start() {
    if(current != null){
        current.show();
        return;
    }
    Form hi = new Form("Hi >>0 World");
     current = hi;
    hi.setLayout(new BorderLayout());
    hi.addComponent(BorderLayout.CENTER, new Label("Hi Overlay World 2") {

        @Override
        public void paint(Graphics g) {
            //g.resetAffine();
            int w = getWidth();
            int h = getHeight();

            g.setColor(0x8f8f9f7f);
            g.fillRect(0,0,w,h);

            g.setColor(0xff);
            g.drawString("clip #"+loops+" "+finishedClip,w/4,h/2);
            if(errorMessage!=null) { g.drawString(errorMessage,w/4,h/2+30); }
        }

    });
    hi.show();
    Runnable rr = new Runnable (){ 
        public void run() {
            System.out.println("running");
        Resources res;
        try {
            res = Resources.open("/theme.res");

            while(true) 
            { 
              try {
              String clip = clips[loops];
              for(int i=0;i<3;i++) { playClip(hi,res,clip); }
                try {
                      Thread.sleep(2000);
                      } catch(InterruptedException e) {};
              }
              catch (Throwable e) { errorMessage = "error: "+e.toString(); }
                  try {
                  Thread.sleep(1000); 
                  } 
                  catch (InterruptedException e) {};
             loops = (loops+1)%clips.length;
            }}
        catch (IOException e1) {
            // TODO Auto-generated catch block
            e1.printStackTrace();
        }

        }

    };

    new Thread(rr).start();
}
public void stop() {
    current = Display.getInstance().getCurrent();
}

public void destroy() {
    }
    }

theme.zip

shannah commented 3 years ago

Interesting. Do you get the same issue if you load the clip from FileSystemStorage rather than the resource file?

ddyer0 commented 3 years ago

Yes, the source of the clip doesn't matter. In another variation, I made a binary in-memory file and read it using a ByteArrayInputStream; no difference.

shannah commented 3 years ago

I'm curious specifically about an input stream created from filesystemstorage. That will follow a different code path.

ddyer0 commented 3 years ago

The version that I actually deploy (rather than the test program I provided) uses this:

        final InputStream instream = data==null 
                    ? new FileInputStream(clipFile)
                    : new ByteArrayInputStream(data);

and arranges for the FileInputStream to be a copy of the resource file.