github.com/noriah/catnip@v1.8.5/input/portaudio/portaudio.go (about)

     1  package portaudio
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  
     8  	"github.com/noriah/catnip/input"
     9  	"github.com/noriah/catnip/input/portaudio/portaudio"
    10  	"github.com/pkg/errors"
    11  )
    12  
    13  func init() {
    14  	input.RegisterBackend("portaudio", &PortBackend{})
    15  }
    16  
    17  // errors
    18  var (
    19  	ErrBadDevice    error = errors.New("device not found")
    20  	ErrReadTimedOut error = errors.New("read timed out")
    21  )
    22  
    23  // PortBackend represents the Portaudio backend. A zero-value instance is a
    24  // valid instance.
    25  type PortBackend struct {
    26  	devices []*portaudio.DeviceInfo
    27  }
    28  
    29  func (b *PortBackend) Init() error {
    30  	return portaudio.Initialize()
    31  }
    32  
    33  func (b *PortBackend) Close() error {
    34  	return portaudio.Terminate()
    35  }
    36  
    37  func (b *PortBackend) Devices() ([]input.Device, error) {
    38  	if b.devices == nil {
    39  		devices, err := portaudio.Devices()
    40  		if err != nil {
    41  			return nil, err
    42  		}
    43  		b.devices = devices
    44  	}
    45  
    46  	gDevices := make([]input.Device, len(b.devices))
    47  	for i, device := range b.devices {
    48  		gDevices[i] = Device{device}
    49  	}
    50  
    51  	return gDevices, nil
    52  }
    53  
    54  func (b *PortBackend) DefaultDevice() (input.Device, error) {
    55  	defaultHost, err := portaudio.DefaultHostApi()
    56  	if err != nil {
    57  		return nil, errors.Wrap(err, "failed to get default host API")
    58  	}
    59  
    60  	if defaultHost.DefaultInputDevice == nil {
    61  		return nil, errors.New("no default input device found")
    62  	}
    63  
    64  	return Device{defaultHost.DefaultInputDevice}, nil
    65  }
    66  
    67  func (b *PortBackend) Start(cfg input.SessionConfig) (input.Session, error) {
    68  	return NewSession(cfg)
    69  }
    70  
    71  // Device represents a Portaudio device.
    72  type Device struct {
    73  	*portaudio.DeviceInfo
    74  }
    75  
    76  func (d *Device) discard() { d.DeviceInfo = nil }
    77  
    78  // String returns the device name.
    79  func (d Device) String() string {
    80  	return d.Name
    81  }
    82  
    83  // SampleType is broken out because portaudio supports different types
    84  type SampleType = float32
    85  
    86  // Session is an input source that pulls from Portaudio.
    87  type Session struct {
    88  	device Device
    89  	config input.SessionConfig
    90  }
    91  
    92  // NewSession creates and initializes a new Portaudio session.
    93  func NewSession(config input.SessionConfig) (*Session, error) {
    94  	dv, ok := config.Device.(Device)
    95  	if !ok {
    96  		return nil, fmt.Errorf("device is on unknown type %T", config.Device)
    97  	}
    98  
    99  	// Free up the device inside the config.
   100  	config.Device = nil
   101  
   102  	return &Session{
   103  		dv,
   104  		config,
   105  	}, nil
   106  }
   107  
   108  func (s *Session) Start(ctx context.Context, dst [][]input.Sample, kickChan chan bool, mu *sync.Mutex) error {
   109  	if !input.EnsureBufferLen(s.config, dst) {
   110  		return errors.New("invalid dst length given")
   111  	}
   112  
   113  	param := portaudio.StreamParameters{
   114  		Input: portaudio.StreamDeviceParameters{
   115  			Device:   s.device.DeviceInfo,
   116  			Latency:  s.device.DefaultLowInputLatency,
   117  			Channels: s.config.FrameSize,
   118  		},
   119  		SampleRate:      s.config.SampleRate,
   120  		FramesPerBuffer: s.config.SampleSize,
   121  		Flags:           portaudio.ClipOff | portaudio.DitherOff,
   122  	}
   123  
   124  	frameSize := s.config.FrameSize
   125  	samples := s.config.SampleSize * frameSize
   126  
   127  	// Source buffer in a different format than what we want (dst).
   128  	src := make([]SampleType, samples)
   129  
   130  	stream, err := portaudio.OpenStream(param, src)
   131  	if err != nil {
   132  		return errors.Wrap(err, "failed to open stream")
   133  	}
   134  	s.device.discard()
   135  	defer stream.Close()
   136  
   137  	if err := stream.Start(); err != nil {
   138  		return errors.Wrap(err, "failed to start stream")
   139  	}
   140  	defer stream.Stop()
   141  
   142  	for {
   143  
   144  		// Ignore overflow in case the processing is too slow.
   145  		if err := stream.Read(); err != nil && err != portaudio.InputOverflowed {
   146  			return errors.Wrap(err, "failed to read stream")
   147  		}
   148  
   149  		mu.Lock()
   150  		for x := 0; x < samples; x++ {
   151  			dst[x%frameSize][x/frameSize] = input.Sample(src[x])
   152  		}
   153  		mu.Unlock()
   154  
   155  		loop := true
   156  		for {
   157  			select {
   158  			case <-ctx.Done():
   159  				return ctx.Err()
   160  			case kickChan <- true:
   161  				loop = false
   162  			default:
   163  			}
   164  
   165  			if ready, _ := stream.AvailableToRead(); !loop || ready >= samples {
   166  				break
   167  			}
   168  		}
   169  	}
   170  }