pyalsaaudio icon indicating copy to clipboard operation
pyalsaaudio copied to clipboard

Missing sample(s) at the end of 'chunks'

Open JohnCC330 opened this issue 8 years ago • 5 comments

The problem reported in a previous issue remains. I've done quite a few tests, capturing samples and reading them in several different ways:

  • Tight loops
  • Threads
  • Poll/Epoll

Each time, samples are lost at chunk boundaries. I've changed sample frequencies from 12000 to 48000 Hz. Change chunksize to different values. Nothing seems to solve the problem. I did a C version of the 'tight loop', which seems to work flawlessly. I can't really imagine that Python wouldn't be able to handle 12000 samples/s, so I don't know if there is some logic mine which is failing.

I'd appreciate pointers to continue experimenting!

Notes on the following program:

  • The input/outputs are set at the top. In my case they correspond to Front Mic and Speaker output. I've connected them together with a cable to test. It's not necessary to do that, but probably it'd good to connect something to the input to see the missing samples.
  • Two methods are in the Generator class. One for Poll, the other uses threads. Select the desired one near the bottom by (un)commenting (in testcalls()).
  • The program generates a 1 second 1000 Hz tone and plots the result using matplotlib.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  lms_sines.py
#
#  Copyright 2017 John Coppens <[email protected]>
#
#  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
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program 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.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#

import alsaaudio
import pylab as plt
import math as m
import numpy as np
import scipy as sp
import cmath
import select
import struct
from   threading import Thread
import sys, time
import pdb

debug = False

MIC_INPUT   = 2
SPKR_OUTPUT = 8
LOG_FNAME   = "test.dat" # None


class In_Thread(Thread):
    def __init__(self, in_pcm, in_frames, channels):
        super(In_Thread, self).__init__()
        self.in_pcm = in_pcm
        self.in_data = []
        self.in_frames = in_frames
        self.channels = channels

    def run(self):
        in_frame = 0
        while in_frame < self.in_frames:
            l, chunk = self.in_pcm.read()
            if l > 0:
                d = list(struct.unpack("<" + "h" * (l * self.channels), chunk))
                self.in_data += d
                in_frame += l
        return


class Out_Thread(Thread):
    def __init__(self, out_pcm, out_data, out_frames, chunksize):
        super(Out_Thread, self).__init__()
        self.out_data = out_data
        self.out_frames = out_frames
        self.out_pcm = out_pcm
        self.chunksize = chunksize

    def run(self):
        out_frame = 0
        while out_frame < self.out_frames*2:
            if self.out_pcm.write(self.out_data[out_frame:
                                                out_frame + self.chunksize]) != 0:
                out_frame += self.chunksize
                print(out_frame)



class Generator():
    """ Generate and record sounds
        Constructor arguments:
            fs          Sample frequency        [48000]
            chunksize   Chunk size in frames    [32]
                        (1 frame is a sample on each channel)
            samplesize  Sample size in bytes    [2]
    """
    def __init__(self, fs = 48000, chunksize = 32, samplesize = 2):
        #self.show_host_apis()
        self.samplerate = fs
        self.samplesize = samplesize
        self.chunksize = chunksize


    def selfcapture_threads(self, f = 1000, in_frames = 48000, channels = 1):
        # Set up recorder
        in_pcm = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, mode = alsaaudio.PCM_NORMAL)
        in_pcm.setchannels(channels)
        in_pcm.setrate(self.samplerate)
        in_pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
        in_pcm.setperiodsize(self.chunksize)           # Period size in FRAMES
        inpcm_fd, inpcm_emask = in_pcm.polldescriptors()[0]

        # Set up generator
        out_pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, mode = alsaaudio.PCM_NORMAL)
        out_pcm.setchannels(1)
        out_pcm.setrate(self.samplerate)
        out_pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
        out_pcm.setperiodsize(self.chunksize)
        outpcm_fd, outpcm_emask = out_pcm.polldescriptors()[0]

        # Generate the entire waveform (as signed 16-bit, little endians)
        omega = 2.0 * m.pi * f / self.samplerate
        out_frames = in_frames
        out_data = b''

        for t in range(out_frames):
            s = int(32760 * m.sin(omega * t))
            out_data += struct.pack("<h", s)

        in_thread  = In_Thread(in_pcm, in_frames, channels)
        out_thread = Out_Thread(out_pcm, out_data, out_frames, self.chunksize)

        in_thread.start()
        out_thread.start()

        in_thread.join()
        out_thread.join()

        return in_thread.in_data



    def selfcapture_poll(self, f = 1000, in_frames = 48000, channels = 1):
        # Set up recorder
        in_pcm = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, mode = alsaaudio.PCM_NONBLOCK)
        in_pcm.setchannels(channels)
        in_pcm.setrate(self.samplerate)
        in_pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
        in_pcm.setperiodsize(self.chunksize)           # Period size in FRAMES
        inpcm_fd, inpcm_emask = in_pcm.polldescriptors()[0]

        # Set up generator
        out_pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, mode = alsaaudio.PCM_NONBLOCK)
        out_pcm.setchannels(1)
        out_pcm.setrate(self.samplerate)
        out_pcm.setformat(alsaaudio.PCM_FORMAT_S16_LE)
        out_pcm.setperiodsize(self.chunksize)
        outpcm_fd, outpcm_emask = out_pcm.polldescriptors()[0]

        # Generate the entire waveform (as signed 16-bit, little endians)
        omega = 2.0 * m.pi * f / self.samplerate
        out_frame = 0
        out_frames = in_frames
        out_data = b''

        for t in range(out_frames):
            s = int(32760 * m.sin(omega * t))
            out_data += struct.pack("<h", s)

        in_data = []

        poller = select.poll()
        if debug:
            print("Descriptors: In: %s, Out: %s" % (in_pcm.polldescriptors(),
                                                    out_pcm.polldescriptors()))
        poller.register(inpcm_fd, inpcm_emask)
        poller.register(outpcm_fd, outpcm_emask)

        l, chunk = in_pcm.read()
        in_frame = 0
        # in_pcm.start()
        while True:
            ready = poller.poll(30)
            if len(ready) == 0:
                if debug: print("End events")
                break

            for fd, event in ready:
                if fd == inpcm_fd:
                    if in_frame < in_frames:
                        l, chunk = in_pcm.read()
                        if l > 0:
                            d = list(struct.unpack("<" + "h" * (l * channels), chunk))
                            in_data += d
                            in_frame += l
                    else:
                        if debug: print("End input")
                        poller.unregister(fd)

                elif fd == outpcm_fd:
                    if out_frame < out_frames*2:
                        if out_pcm.write(out_data[out_frame:
                                                  out_frame + self.chunksize]) != 0:
                            out_frame += self.chunksize
                    else:
                        if debug: print("End output")
                        poller.unregister(fd)
        return in_data


    def plot_data(self, data, channels = 1, plot_time = True):
        """ Data are the data samples (16-bit only for now),
            interleaved Left/Right
        """
        xlen = len(data) // channels    # xlen = 96000 = 48000
        data = data[:xlen*channels]

        x = []
        for i in range(xlen):
            if plot_time:
                x.append(i/self.samplerate)
            else:
                x.append(i)

        try:
            if channels == 1:
                plt.plot(x, data)
            elif channels == 2:
                plt.plot(x, data[0::2])
                plt.plot(x, data[1::2])
        except:
            print("len(x): %d  len(y): %d" % (len(x), len(data)))

        plt.xlabel("Time (%s)" % ("s" if plot_time else "samples"))
        plt.ylabel("Signal (16bit signed)")
        plt.grid(True)
        plt.show()


    def print_data(self, data):
        for y in data:
            print("%6d " % y, end = '')
        print()


def test_calls():
    gen = Generator(chunksize = 64, fs = 48000)
    #data = gen.selfcapture_threads(1000, 48000, channels = 2)
    data = gen.selfcapture_poll(1000, 48000, channels = 2)
    gen.plot_data(data, channels = 2, plot_time = False)


def main(args):
    test_calls()
    return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))

JohnCC330 avatar Apr 11 '17 20:04 JohnCC330

In the code above, lots of code is duplicated because of the two methods (threads and polling) implemented.

Here are some screenshots of the output. These is the generated sine wave, reproduced and (using a cable) re-introduced (to the left and right mic inputs, plotted as blue and orange - not easy to distinguish), recorded and plotted:

screenshot_2017-04-11_16-57-51 (This is a single lost sample, the value from 'read' was 0 for both channels)

screenshot_2017-04-11_16-57-09 (An example of a double error)

To be sure the sine was was generated correctly, I connected the output to another machine, and recorded it there using Audacity.

JohnCC330 avatar Apr 12 '17 19:04 JohnCC330

is this still reproducible?

ossilator avatar Aug 05 '22 11:08 ossilator

The code posted by the OP claims to be python3 code, so we can expect it to work. Have you tried?

There is a small chance that the fix I provided for #51 also fixes this. But I would have to spend more time to see whether these problems are the same.

RonaldAJ avatar Aug 26 '22 14:08 RonaldAJ

With the changes below the OP code runs again. But the figures don't reproduce on my system, this might require some alsa configuration to work. (Commit used: 6317d9ad on main)

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  lms_sines.py
#
#  Copyright 2017 John Coppens <[email protected]>
#
#  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
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program 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.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#
#

import alsaaudio
import pylab as plt
import math as m
import numpy as np
import scipy as sp
import cmath
import select
import struct
from   threading import Thread
import sys, time
import pdb

debug = True

MIC_INPUT   = 2
SPKR_OUTPUT = 8
LOG_FNAME   = "test.dat" # None


class In_Thread(Thread):
    def __init__(self, in_pcm, in_frames, channels):
        super(In_Thread, self).__init__()
        self.in_pcm = in_pcm
        self.in_data = []
        self.in_frames = in_frames
        self.channels = channels

    def run(self):
        in_frame = 0
        while in_frame < self.in_frames:
            l, chunk = self.in_pcm.read()
            if l > 0:
                d = list(struct.unpack("<" + "h" * (l * self.channels), chunk))
                self.in_data += d
                in_frame += l
        return


class Out_Thread(Thread):
    def __init__(self, out_pcm, out_data, out_frames, chunksize):
        super(Out_Thread, self).__init__()
        self.out_data = out_data
        self.out_frames = out_frames
        self.out_pcm = out_pcm
        self.chunksize = chunksize

    def run(self):
        out_frame = 0
        while out_frame < self.out_frames*2:
            if self.out_pcm.write(self.out_data[out_frame:
                                                out_frame + self.chunksize]) != 0:
                out_frame += self.chunksize
                print(out_frame)



class Generator():
    """ Generate and record sounds
        Constructor arguments:
            fs          Sample frequency        [48000]
            chunksize   Chunk size in frames    [32]
                        (1 frame is a sample on each channel)
            samplesize  Sample size in bytes    [2]
    """
    def __init__(self, fs = 48000, chunksize = 32, samplesize = 2):
        #self.show_host_apis()
        self.samplerate = fs
        self.samplesize = samplesize
        self.chunksize = chunksize


    def selfcapture_threads(self, f = 1000, in_frames = 48000, channels = 1):
        # Set up recorder
        in_pcm = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, 
                               mode = alsaaudio.PCM_NORMAL,
                               channels = channels,
                               rate = self.samplerate,
                               format = alsaaudio.PCM_FORMAT_S16_LE,
                               periodsize = self.chunksize,
                               )
                               
        inpcm_fd, inpcm_emask = in_pcm.polldescriptors()[0]

        # Set up generator
        out_pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, 
                               mode = alsaaudio.PCM_NORMAL,
                               channels = 1,
                               rate = self.samplerate,
                               format = alsaaudio.PCM_FORMAT_S16_LE,
                               periodsize = self.chunksize,)
                               
        outpcm_fd, outpcm_emask = out_pcm.polldescriptors()[0]

        # Generate the entire waveform (as signed 16-bit, little endians)
        omega = 2.0 * m.pi * f / self.samplerate
        out_frames = in_frames
        out_data = b''

        for t in range(out_frames):
            s = int(32760 * m.sin(omega * t))
            out_data += struct.pack("<h", s)

        in_thread  = In_Thread(in_pcm, in_frames, channels)
        out_thread = Out_Thread(out_pcm, out_data, out_frames, self.chunksize)

        in_thread.start()
        out_thread.start()

        in_thread.join()
        out_thread.join()

        return in_thread.in_data



    def selfcapture_poll(self, f = 1000, in_frames = 48000, channels = 1):
        # Set up recorder
        in_pcm = alsaaudio.PCM(alsaaudio.PCM_CAPTURE, 
                                mode = alsaaudio.PCM_NONBLOCK,
                               channels = channels,
                               rate = self.samplerate,
                               format = alsaaudio.PCM_FORMAT_S16_LE,
                               periodsize = self.chunksize,)
                               
        inpcm_fd, inpcm_emask = in_pcm.polldescriptors()[0]

        # Set up generator
        out_pcm = alsaaudio.PCM(alsaaudio.PCM_PLAYBACK, 
                                mode = alsaaudio.PCM_NONBLOCK,
                                channels = 1,
                                rate = self.samplerate,
                                format = alsaaudio.PCM_FORMAT_S16_LE,
                                periodsize = self.chunksize,)
       
        outpcm_fd, outpcm_emask = out_pcm.polldescriptors()[0]

        # Generate the entire waveform (as signed 16-bit, little endians)
        omega = 2.0 * m.pi * f / self.samplerate
        out_frame = 0
        out_frames = in_frames
        out_data = b''

        for t in range(out_frames):
            s = int(32760 * m.sin(omega * t))
            out_data += struct.pack("<h", s)

        in_data = []

        poller = select.poll()
        if debug:
            print("Descriptors: In: %s, Out: %s" % (in_pcm.polldescriptors(),
                                                    out_pcm.polldescriptors()))
        poller.register(inpcm_fd, inpcm_emask)
        poller.register(outpcm_fd, outpcm_emask)

        l, chunk = in_pcm.read()
        in_frame = 0
        # in_pcm.start()
        while True:
            ready = poller.poll(30)
            if len(ready) == 0:
                if debug: print("End events")
                break

            for fd, event in ready:
                if fd == inpcm_fd:
                    if in_frame < in_frames:
                        l, chunk = in_pcm.read()
                        if l > 0:
                            d = list(struct.unpack("<" + "h" * (l * channels), chunk))
                            in_data += d
                            in_frame += l
                    else:
                        if debug: print("End input")
                        poller.unregister(fd)

                elif fd == outpcm_fd:
                    if out_frame < out_frames*2:
                        if out_pcm.write(out_data[out_frame:
                                                  out_frame + self.chunksize]) != 0:
                            out_frame += self.chunksize
                    else:
                        if debug: print("End output")
                        poller.unregister(fd)
        return in_data


    def plot_data(self, data, channels = 1, plot_time = True):
        """ Data are the data samples (16-bit only for now),
            interleaved Left/Right
        """
        xlen = len(data) // channels    # xlen = 96000 = 48000
        data = data[:xlen*channels]

        x = []
        for i in range(xlen):
            if plot_time:
                x.append(i/self.samplerate)
            else:
                x.append(i)

        try:
            if channels == 1:
                plt.plot(x, data)
            elif channels == 2:
                plt.plot(x, data[0::2])
                plt.plot(x, data[1::2])
        except:
            print("len(x): %d  len(y): %d" % (len(x), len(data)))

        plt.xlabel("Time (%s)" % ("s" if plot_time else "samples"))
        plt.ylabel("Signal (16bit signed)")
        plt.grid(True)
        plt.show()


    def print_data(self, data):
        for y in data:
            print("%6d " % y, end = '')
        print()


def test_calls():
    gen = Generator(chunksize = 64, fs = 48000)
    #data = gen.selfcapture_threads(1000, 48000, channels = 2)
    data = gen.selfcapture_poll(1000, 48000, channels = 2)
    gen.plot_data(data, channels = 2, plot_time = False)


def main(args):
    test_calls()
    return 0

if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))

RonaldAJ avatar Aug 26 '22 14:08 RonaldAJ

The previous issue mentioned is probably: #32

RonaldAJ avatar Aug 26 '22 15:08 RonaldAJ

as OP apparently lost interest, i'm closing this.

i didn't study the test code in detail, because it's way too complex. - if there is an actual problem in the library, it can be reproduced much more easily. if the complexity seems necessary for reproduction, then either the bug is in the user code, or the real trigger is most likely system load of some kind (cpu, memory, device activity (interrupts), etc.).

the pattern in the plots does not indicate that samples are lost or added, but rather that the buffer is overwritten or not written in the first place. i suspect a bug in a lower layer (hardware, driver, libalsa), and hope it was fixed or worked around in the right place meanwhile.

ossilator avatar Feb 01 '24 18:02 ossilator