blob: b0f67d4727d6cf9e163532fd5afe9e60a261d422 [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 Chiang17a25272014-11-28 19:05:13 +080033class AudioRawDataError(Exception):
34 """Error in AudioRawData."""
35 pass
36
37
Cheng-Yi Chiang348270a2014-09-01 19:59:38 +080038class AudioRawData(object):
39 """The abstraction of audio raw data.
40
41 @property channel: The number of channels.
42 @property channel_data: A list of lists containing samples in each channel.
43 E.g., The third sample in the second channel is
44 channel_data[1][2].
45 @property sample_format: The sample format which should be one of the keys
46 in audio_data.SAMPLE_FORMATS.
47 """
48 def __init__(self, binary, channel, sample_format):
49 """Initializes an AudioRawData.
50
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +080051 @param binary: A string containing binary data. If binary is not None,
52 The samples in binary will be parsed and be filled into
53 channel_data.
Cheng-Yi Chiang348270a2014-09-01 19:59:38 +080054 @param channel: The number of channels.
55 @param sample_format: One of the keys in audio_data.SAMPLE_FORMATS.
56 """
57 self.channel = channel
58 self.channel_data = [[] for _ in xrange(self.channel)]
59 self.sample_format = sample_format
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +080060 if binary:
61 self.read_binary(binary)
Cheng-Yi Chiang348270a2014-09-01 19:59:38 +080062
63
64 def read_one_sample(self, handle):
65 """Reads one sample from handle.
66
67 @param handle: A handle that supports read() method.
68
69 @return: A number read from file handle based on sample format.
70 None if there is no data to read.
71 """
72 data = handle.read(SAMPLE_FORMATS[self.sample_format]['size_bytes'])
73 if data == '':
74 return None
75 number, = struct.unpack(
76 SAMPLE_FORMATS[self.sample_format]['struct_format'], data)
77 return number
78
79
80 def read_binary(self, binary):
81 """Reads samples from binary and fills channel_data.
82
83 Reads one sample for each channel and repeats until the end of
84 input binary.
85
86 @param binary: A string containing binary data.
87 """
88 channel_index = 0
89 with contextlib.closing(StringIO.StringIO(binary)) as f:
90 number = self.read_one_sample(f)
91 while number is not None:
92 self.channel_data[channel_index].append(number)
93 channel_index = (channel_index + 1) % self.channel
94 number = self.read_one_sample(f)
Cheng-Yi Chiang17a25272014-11-28 19:05:13 +080095
96
97 def copy_channel_data(self, channel_data):
98 """Copies channel data and updates channel number.
99
100 @param channel_data: A list of list. The channel data to be copied.
101
102 """
103 self.channel_data = copy.deepcopy(channel_data)
104 self.channel = len(self.channel_data)
105
106
107 def write_to_file(self, file_path):
108 """Writes channel data to file.
109
110 Writes samples in each channel into file in index-first sequence.
111 E.g. (index_0, ch_0), (index_0, ch_1), ... ,(index_0, ch_N),
112 (index_1, ch_0), (index_1, ch_1), ... ,(index_1, ch_N).
113
114 @param file_path: The path to the file.
115
116 """
117 lengths = [len(self.channel_data[ch])
118 for ch in xrange(self.channel)]
119 if len(set(lengths)) != 1:
120 raise AudioRawDataError(
121 'Channel lengths are not the same: %r' % lengths)
122 length = lengths[0]
123
124 with open(file_path, 'wb') as f:
125 for index in xrange(length):
126 for ch in xrange(self.channel):
127 f.write(struct.pack(
128 SAMPLE_FORMATS[self.sample_format]['struct_format'],
129 self.channel_data[ch][index]))