blob: 3cc150da349711504becf4792e139c6f51a8592c [file] [log] [blame]
Cheng-Yi Chiang348270a2014-09-01 19:59:38 +08001#!/usr/bin/env python
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +08002# Copyright 2014 The Chromium OS Authors. All rights reserved.
Cheng-Yi Chiang348270a2014-09-01 19:59:38 +08003# 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
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +08009import copy
Cheng-Yi Chiang348270a2014-09-01 19:59:38 +080010import 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
Cheng-Yi Chiang8302dbf2015-11-19 12:56:25 +080033def 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
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +080045class AudioRawDataError(Exception):
46 """Error in AudioRawData."""
47 pass
48
49
Cheng-Yi Chiang348270a2014-09-01 19:59:38 +080050class 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
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +080063 @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.
Cheng-Yi Chiang348270a2014-09-01 19:59:38 +080066 @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
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +080072 if binary:
73 self.read_binary(binary)
Cheng-Yi Chiang348270a2014-09-01 19:59:38 +080074
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)
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +0800107
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]))