blob: 72883b249c9410876c0a5fb8172ac778a7264027 [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package audio interacts with audio operation.
package audio
import (
"context"
"fmt"
"time"
"github.com/godbus/dbus"
"chromiumos/tast/errors"
"chromiumos/tast/local/dbusutil"
"chromiumos/tast/testing"
)
const (
dbusName = "org.chromium.cras"
dbusPath = "/org/chromium/cras"
dbusInterface = "org.chromium.cras.Control"
)
// StreamType is used to specify the type of node we want to use for tests and
// helper functions.
type StreamType uint
const (
// InputStream describes nodes with true IsInput attributes.
InputStream StreamType = 1 << iota
// OutputStream describes nodes with false IsInput attributes.
OutputStream
)
func (t StreamType) String() string {
switch t {
case InputStream:
return "InputStream"
case OutputStream:
return "OutputStream"
default:
return fmt.Sprintf("StreamType(%#x)", t)
}
}
// Cras is used to interact with the cras process over D-Bus.
// For detailed spec, please find src/third_party/adhd/cras/README.dbus-api.
type Cras struct {
obj dbus.BusObject
}
// NewCras connects to CRAS via D-Bus and returns a Cras object.
func NewCras(ctx context.Context) (*Cras, error) {
testing.ContextLogf(ctx, "Waiting for %s D-Bus service", dbusName)
_, obj, err := dbusutil.Connect(ctx, dbusName, dbusPath)
if err != nil {
return nil, err
}
return &Cras{obj}, nil
}
// CrasNode contains the metadata of Node in Cras.
// Currently fields which are actually needed by tests are defined.
// Please find src/third_party/adhd/cras/README.dbus-api for the meaning of
// each fields.
type CrasNode struct {
ID uint64
Type string
Active bool
IsInput bool
DeviceName string
NodeVolume uint64
}
// VolumeState contains the metadata of volume state in Cras.
// Currently fields which are actually needed by tests are defined.
// Please find src/third_party/adhd/cras/dbus_bindings/org.chromium.cras.Control.xml
// for the meaning of each fields.
type VolumeState struct {
OutputVol int
OutputMute bool
InputMute bool
OutputUserMute bool
}
// GetNodes calls cras.Control.GetNodes over D-Bus.
func (c *Cras) GetNodes(ctx context.Context) ([]CrasNode, error) {
call := c.call(ctx, "GetNodes")
if call.Err != nil {
return nil, call.Err
}
// cras.Control.GetNodes D-Bus method's signature is not fixed.
// Specifically, the number of output values depends on the actual
// number of nodes.
// That usage is not common practice, and it is less supported in
// godbus. Here, instead, values are manually converted via
// dbus.Variant.
nodes := make([]CrasNode, len(call.Body))
for i, n := range call.Body {
mp := n.(map[string]dbus.Variant)
if id, ok := mp["Id"]; !ok {
return nil, errors.Errorf("'Id' not found: %v", mp)
} else if nodes[i].ID, ok = id.Value().(uint64); !ok {
return nil, errors.Errorf("'Id' is not uint64: %v", mp)
}
if nodeType, ok := mp["Type"]; !ok {
return nil, errors.Errorf("'Type' not found: %v", mp)
} else if nodes[i].Type, ok = nodeType.Value().(string); !ok {
return nil, errors.Errorf("'Type' is not string: %v", mp)
}
if active, ok := mp["Active"]; !ok {
return nil, errors.Errorf("'Active' not found: %v", mp)
} else if nodes[i].Active, ok = active.Value().(bool); !ok {
return nil, errors.Errorf("'Active' is not bool: %v", mp)
}
if isInput, ok := mp["IsInput"]; !ok {
return nil, errors.Errorf("'IsInput' not found: %v", mp)
} else if nodes[i].IsInput, ok = isInput.Value().(bool); !ok {
return nil, errors.Errorf("'IsInput' is not bool: %v", mp)
}
if deviceName, ok := mp["DeviceName"]; !ok {
return nil, errors.Errorf("'DeviceName' not found: %v", mp)
} else if nodes[i].DeviceName, ok = deviceName.Value().(string); !ok {
return nil, errors.Errorf("'DeviceName' is not string: %v", mp)
}
if nodeVolume, ok := mp["NodeVolume"]; !ok {
return nil, errors.Errorf("'NodeVolume' not found: %v", mp)
} else if nodes[i].NodeVolume, ok = nodeVolume.Value().(uint64); !ok {
return nil, errors.Errorf("'NodeVolume' is not uint64: %v", mp)
}
}
return nodes, nil
}
// GetNodeByType returns the first node with given type.
func (c *Cras) GetNodeByType(ctx context.Context, t string) (*CrasNode, error) {
nodes, err := c.GetNodes(ctx)
if err != nil {
return nil, err
}
for _, n := range nodes {
if n.Type == t {
return &n, nil
}
// Regard the front mic as the internal mic.
if t == "INTERNAL_MIC" && n.Type == "FRONT_MIC" {
return &n, nil
}
}
return nil, errors.Errorf("failed to find a node with type %s", t)
}
// call is a wrapper around CallWithContext for convenience.
func (c *Cras) call(ctx context.Context, method string, args ...interface{}) *dbus.Call {
return c.obj.CallWithContext(ctx, dbusInterface+"."+method, 0, args...)
}
// SetActiveNode calls cras.Control.SetActiveInput(Output)Node over D-Bus.
func (c *Cras) SetActiveNode(ctx context.Context, node CrasNode) error {
cmd := "SetActiveOutputNode"
if node.IsInput {
cmd = "SetActiveInputNode"
}
return c.call(ctx, cmd, node.ID).Err
}
// SetActiveNodeByType sets node with specified type active.
func (c *Cras) SetActiveNodeByType(ctx context.Context, nodeType string) error {
var node *CrasNode
// Wait until the node with this type is existing.
if err := testing.Poll(ctx, func(ctx context.Context) error {
n, err := c.GetNodeByType(ctx, nodeType)
node = n
return err
}, &testing.PollOptions{Timeout: time.Second}); err != nil {
return errors.Errorf("failed to wait node %s", nodeType)
}
if err := c.SetActiveNode(ctx, *node); err != nil {
return errors.Errorf("failed to set node %s active", nodeType)
}
// Wait until that node is active.
if err := testing.Poll(ctx, func(ctx context.Context) error {
n, err := c.GetNodeByType(ctx, nodeType)
if err != nil {
return err
}
if !n.Active {
return errors.New("node is not active")
}
return nil
}, &testing.PollOptions{Timeout: time.Second}); err != nil {
return errors.Errorf("failed to wait node %s to be active", nodeType)
}
return nil
}
// SetOutputNodeVolume calls cras.Control.SetOutputNodeVolume over D-Bus.
func (c *Cras) SetOutputNodeVolume(ctx context.Context, node CrasNode, volume int) error {
return c.call(ctx, "SetOutputNodeVolume", node.ID, volume).Err
}
// GetVolumeState calls cras.Control.GetVolumeState over D-Bus.
func (c *Cras) GetVolumeState(ctx context.Context) (*VolumeState, error) {
var vol int32
var outputMute, inputMute, outputUserMute bool
err := c.call(ctx, "GetVolumeState").Store(&vol, &outputMute, &inputMute, &outputUserMute)
if err != nil {
return nil, err
}
return &VolumeState{
OutputVol: int(vol),
OutputMute: outputMute,
InputMute: inputMute,
OutputUserMute: outputUserMute,
}, nil
}
// WaitForDeviceUntil waits until any cras node meets the given condition.
// condition is a function that takes a cras node as input and returns true if the node status
// satisfies the criteria.
func (c *Cras) WaitForDeviceUntil(ctx context.Context, condition func(*CrasNode) bool, timeout time.Duration) error {
return testing.Poll(ctx, func(ctx context.Context) error {
nodes, err := c.GetNodes(ctx)
if err != nil {
return err
}
for _, n := range nodes {
if condition(&n) {
return nil
}
}
return errors.New("cras node(s) not in requested condition")
}, &testing.PollOptions{Timeout: timeout, Interval: 1 * time.Second})
}
// WaitForDevice waits for specified types of stream nodes to be active.
// You can pass the streamType as a bitmap to wait for both input and output
// nodes to be active. Ex: WaitForDevice(ctx, InputStream|OutputStream)
// It should be used to verify the target types of nodes exist and are
// active before the real test starts.
// Notice that some devices use their displays as an internal speaker
// (e.g. monroe). When a display is closed, the internal speaker is removed,
// too. For this case, we should call power.TurnOnDisplay to turn on a display
// to re-enable an internal speaker.
func WaitForDevice(ctx context.Context, streamType StreamType) error {
cras, err := NewCras(ctx)
if err != nil {
return err
}
var active StreamType
checkActiveNode := func(n *CrasNode) bool {
if !n.Active {
return false
}
if n.IsInput {
active |= InputStream
} else {
active |= OutputStream
}
return streamType&active == streamType
}
return cras.WaitForDeviceUntil(ctx, checkActiveNode, 10*time.Second)
}
// SelectedOutputDevice returns the active output device name and type.
func (c *Cras) SelectedOutputDevice(ctx context.Context) (deviceName, deviceType string, err error) {
nodes, err := c.GetNodes(ctx)
if err != nil {
return
}
for _, node := range nodes {
if node.Active && !node.IsInput {
deviceName = node.DeviceName
deviceType = node.Type
break
}
}
return
}