mirror of
https://github.com/pion/mediadevices.git
synced 2025-10-04 08:16:33 +08:00
205 lines
4.8 KiB
Go
205 lines
4.8 KiB
Go
package microphone
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/gen2brain/malgo"
|
|
"github.com/pion/mediadevices/internal/logging"
|
|
"github.com/pion/mediadevices/pkg/driver"
|
|
"github.com/pion/mediadevices/pkg/io/audio"
|
|
"github.com/pion/mediadevices/pkg/prop"
|
|
"github.com/pion/mediadevices/pkg/wave"
|
|
)
|
|
|
|
const (
|
|
maxDeviceIDLength = 20
|
|
// TODO: should replace this with a more flexible approach
|
|
sampleRateStep = 1000
|
|
initialBufferSize = 1024
|
|
)
|
|
|
|
var logger = logging.NewLogger("mediadevices/driver/microphone")
|
|
var ctx *malgo.AllocatedContext
|
|
var hostEndian binary.ByteOrder
|
|
var (
|
|
errUnsupportedFormat = errors.New("the provided audio format is not supported")
|
|
)
|
|
|
|
type microphone struct {
|
|
malgo.DeviceInfo
|
|
chunkChan chan []byte
|
|
}
|
|
|
|
func init() {
|
|
var err error
|
|
/*
|
|
backends := []malgo.Backend{
|
|
malgo.BackendPulseaudio,
|
|
malgo.BackendAlsa,
|
|
}
|
|
*/
|
|
ctx, err = malgo.InitContext(nil, malgo.ContextConfig{}, func(message string) {
|
|
logger.Debugf("%v\n", message)
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
devices, err := ctx.Devices(malgo.Capture)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for _, device := range devices {
|
|
// TODO: Detect default device and prioritize it
|
|
driver.GetManager().Register(newMicrophone(device), driver.Info{
|
|
Label: device.ID.String(),
|
|
DeviceType: driver.Microphone,
|
|
})
|
|
}
|
|
|
|
// Decide which endian
|
|
switch v := *(*uint16)(unsafe.Pointer(&([]byte{0x12, 0x34}[0]))); v {
|
|
case 0x1234:
|
|
hostEndian = binary.BigEndian
|
|
case 0x3412:
|
|
hostEndian = binary.LittleEndian
|
|
default:
|
|
panic(fmt.Sprintf("failed to determine host endianness: %x", v))
|
|
}
|
|
}
|
|
|
|
func newMicrophone(info malgo.DeviceInfo) *microphone {
|
|
return µphone{
|
|
DeviceInfo: info,
|
|
}
|
|
}
|
|
|
|
func (m *microphone) Open() error {
|
|
m.chunkChan = make(chan []byte, 1)
|
|
return nil
|
|
}
|
|
|
|
func (m *microphone) Close() error {
|
|
if m.chunkChan != nil {
|
|
close(m.chunkChan)
|
|
m.chunkChan = nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *microphone) AudioRecord(inputProp prop.Media) (audio.Reader, error) {
|
|
var config malgo.DeviceConfig
|
|
var callbacks malgo.DeviceCallbacks
|
|
|
|
decoder, err := wave.NewDecoder(&wave.RawFormat{
|
|
SampleSize: inputProp.SampleSize,
|
|
IsFloat: inputProp.IsFloat,
|
|
Interleaved: inputProp.IsInterleaved,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
config.DeviceType = malgo.Capture
|
|
config.PerformanceProfile = malgo.LowLatency
|
|
config.Capture.Channels = uint32(inputProp.ChannelCount)
|
|
config.SampleRate = uint32(inputProp.SampleRate)
|
|
if inputProp.SampleSize == 4 && inputProp.IsFloat {
|
|
config.Capture.Format = malgo.FormatF32
|
|
} else if inputProp.SampleSize == 2 && !inputProp.IsFloat {
|
|
config.Capture.Format = malgo.FormatS16
|
|
} else {
|
|
return nil, errUnsupportedFormat
|
|
}
|
|
|
|
onRecvChunk := func(_, chunk []byte, framecount uint32) {
|
|
m.chunkChan <- chunk
|
|
}
|
|
callbacks.Data = onRecvChunk
|
|
|
|
device, err := malgo.InitDevice(ctx.Context, config, callbacks)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = device.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return audio.ReaderFunc(func() (wave.Audio, func(), error) {
|
|
chunk, ok := <-m.chunkChan
|
|
if !ok {
|
|
device.Stop()
|
|
device.Uninit()
|
|
return nil, func() {}, io.EOF
|
|
}
|
|
|
|
decodedChunk, err := decoder.Decode(hostEndian, chunk, inputProp.ChannelCount)
|
|
// FIXME: the decoder should also fill this information
|
|
decodedChunk.(*wave.Float32Interleaved).Size.SamplingRate = inputProp.SampleRate
|
|
return decodedChunk, func() {}, err
|
|
}), nil
|
|
}
|
|
|
|
func (m *microphone) Properties() []prop.Media {
|
|
var supportedProps []prop.Media
|
|
logger.Debug("Querying properties")
|
|
|
|
var isBigEndian bool
|
|
// miniaudio only uses the host endian
|
|
if hostEndian == binary.BigEndian {
|
|
isBigEndian = true
|
|
}
|
|
|
|
for ch := m.MinChannels; ch <= m.MaxChannels; ch++ {
|
|
for sampleRate := m.MinSampleRate; sampleRate <= m.MaxSampleRate; sampleRate += sampleRateStep {
|
|
for i := 0; i < int(m.FormatCount); i++ {
|
|
format := m.Formats[i]
|
|
|
|
supportedProp := prop.Media{
|
|
Audio: prop.Audio{
|
|
ChannelCount: int(ch),
|
|
SampleRate: int(sampleRate),
|
|
IsBigEndian: isBigEndian,
|
|
// miniaudio only supports interleaved at the moment
|
|
IsInterleaved: true,
|
|
},
|
|
}
|
|
|
|
switch malgo.FormatType(format) {
|
|
case malgo.FormatF32:
|
|
supportedProp.SampleSize = 4
|
|
supportedProp.IsFloat = true
|
|
case malgo.FormatS16:
|
|
supportedProp.SampleSize = 2
|
|
supportedProp.IsFloat = false
|
|
}
|
|
|
|
supportedProps = append(supportedProps, supportedProp)
|
|
}
|
|
}
|
|
}
|
|
|
|
// FIXME: remove this hardcoded value. Malgo doesn't support "ma_context_get_device_info" API yet. The above iterations
|
|
// will always return nothing as of now
|
|
supportedProps = append(supportedProps, prop.Media{
|
|
Audio: prop.Audio{
|
|
Latency: time.Millisecond * 20,
|
|
ChannelCount: 1,
|
|
SampleRate: 48000,
|
|
SampleSize: 4,
|
|
IsFloat: true,
|
|
IsBigEndian: isBigEndian,
|
|
IsInterleaved: true,
|
|
},
|
|
})
|
|
return supportedProps
|
|
}
|