1#!/usr/bin/env python
2# Copyright 2014 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""This module provides abstraction of audio data."""
7
8import contextlib
9import copy
10import struct
11import StringIO
12
13
14"""The dict containing information on how to parse sample from raw data.
15
16Keys: The sample format as in aplay command.
17Values: A dict containing:
18    message: Human-readable sample format.
19    struct_format: Format used in struct.unpack.
20    size_bytes: Number of bytes for one sample.
21"""
22SAMPLE_FORMATS = dict(
23        S32_LE=dict(
24                message='Signed 32-bit integer, little-endian',
25                struct_format='<i',
26                size_bytes=4),
27        S16_LE=dict(
28                message='Signed 16-bit integer, little-endian',
29                struct_format='<h',
30                size_bytes=2))
31
32
33def get_maximum_value_from_sample_format(sample_format):
34    """Gets the maximum value from sample format.
35
36    @param sample_format: A key in SAMPLE_FORMAT.
37
38    @returns: The maximum value the sample can hold + 1.
39
40    """
41    size_bits = SAMPLE_FORMATS[sample_format]['size_bytes'] * 8
42    return 1 << (size_bits - 1)
43
44
45class AudioRawDataError(Exception):
46    """Error in AudioRawData."""
47    pass
48
49
50class AudioRawData(object):
51    """The abstraction of audio raw data.
52
53    @property channel: The number of channels.
54    @property channel_data: A list of lists containing samples in each channel.
55                            E.g., The third sample in the second channel is
56                            channel_data[1][2].
57    @property sample_format: The sample format which should be one of the keys
58                             in audio_data.SAMPLE_FORMATS.
59    """
60    def __init__(self, binary, channel, sample_format):
61        """Initializes an AudioRawData.
62
63        @param binary: A string containing binary data. If binary is not None,
64                       The samples in binary will be parsed and be filled into
65                       channel_data.
66        @param channel: The number of channels.
67        @param sample_format: One of the keys in audio_data.SAMPLE_FORMATS.
68        """
69        self.channel = channel
70        self.channel_data = [[] for _ in xrange(self.channel)]
71        self.sample_format = sample_format
72        if binary:
73            self.read_binary(binary)
74
75
76    def read_one_sample(self, handle):
77        """Reads one sample from handle.
78
79        @param handle: A handle that supports read() method.
80
81        @return: A number read from file handle based on sample format.
82                 None if there is no data to read.
83        """
84        data = handle.read(SAMPLE_FORMATS[self.sample_format]['size_bytes'])
85        if data == '':
86            return None
87        number, = struct.unpack(
88                SAMPLE_FORMATS[self.sample_format]['struct_format'], data)
89        return number
90
91
92    def read_binary(self, binary):
93        """Reads samples from binary and fills channel_data.
94
95        Reads one sample for each channel and repeats until the end of
96        input binary.
97
98        @param binary: A string containing binary data.
99        """
100        channel_index = 0
101        with contextlib.closing(StringIO.StringIO(binary)) as f:
102            number = self.read_one_sample(f)
103            while number is not None:
104                self.channel_data[channel_index].append(number)
105                channel_index = (channel_index + 1) % self.channel
106                number = self.read_one_sample(f)
107
108
109    def copy_channel_data(self, channel_data):
110        """Copies channel data and updates channel number.
111
112        @param channel_data: A list of list. The channel data to be copied.
113
114        """
115        self.channel_data = copy.deepcopy(channel_data)
116        self.channel = len(self.channel_data)
117
118
119    def write_to_file(self, file_path):
120        """Writes channel data to file.
121
122        Writes samples in each channel into file in index-first sequence.
123        E.g. (index_0, ch_0), (index_0, ch_1), ... ,(index_0, ch_N),
124             (index_1, ch_0), (index_1, ch_1), ... ,(index_1, ch_N).
125
126        @param file_path: The path to the file.
127
128        """
129        lengths = [len(self.channel_data[ch])
130                   for ch in xrange(self.channel)]
131        if len(set(lengths)) != 1:
132            raise AudioRawDataError(
133                    'Channel lengths are not the same: %r' % lengths)
134        length = lengths[0]
135
136        with open(file_path, 'wb') as f:
137            for index in xrange(length):
138                for ch in xrange(self.channel):
139                    f.write(struct.pack(
140                            SAMPLE_FORMATS[self.sample_format]['struct_format'],
141                            self.channel_data[ch][index]))
142