python WAV音频文件处理—— (2)处理PCM音频-- waveio包

news2024/10/7 18:29:29

破译 PCM-Encoded 的音频样本

这部分将变得稍微高级一些,但从长远来看,它将使在 Python 中处理 WAV 文件变得更加容易。
在本教程结束时,我们将构建出 waveio 包:

waveio/
│
├── __init__.py
├── encoding.py
├── metadata.py
├── reader.py
└── writer.py
  • encoding 模块将负责归一化幅度值和 PCM 编码样本之间的双向转换
  • metadata 模块将表示 WAV 文件头
  • reader 读取和解释音频帧
  • writer 写入 WAV 文件

枚举编码格式

waveio/encoding.py
创建PCMEncoding类继承枚举类IntEnum,并实现max, min, num_bits方法。

from enum import IntEnum

class PCMEncoding(IntEnum):
    UNSIGNED_8 = 1
    SIGNED_16 = 2
    SIGNED_24 = 3
    SIGNED_32 = 4

    @property
    def max(self):
        return 255 if self == 1 else -self.min -1
    
    @property
    def min(self):
        return 0 if self == 1 else -(2** (self.num_bits-1))
    
    @property
    def num_bits(self):
        return self * 8

Docode 将音频帧转换为振幅

继续向 PCMEncoding 类添加一个新方法decode,该方法将处理四种编码格式,将帧转换成(归一化的)振幅。

from enum import IntEnum
import numpy as np

class PCMEncoding(IntEnum):
    # ...

    def decode(self, frames):
        match self:
            case PCMEncoding.UNSIGNED_8:
                return np.frombuffer(frames, "u1") / self.max * 2 - 1
            case PCMEncoding.SIGNED_16:
            	# little-endin 2-byte signed integer 
                return np.frombuffer(frames, "<i2") / -self.min
            case PCMEncoding.SIGNED_24:
                triplets = np.frombuffer(frames, "u1").reshape(-1, 3)
                padded = np.pad(triplets, ((0, 0), (0, 1)), mode="constant")
                samples = padded.flatten().view("<i4")
                samples[samples > self.max] += 2 * self.min
                return samples / -self.min
            case PCMEncoding.SIGNED_32:
                return np.frombuffer(frames, "<i4") / -self.min
            case _:
                raise TypeError("unsupported encoding")

Encode 将振幅编码为音频帧

添加.encoder()方法,将振幅转换成帧。

from enum import IntEnum

import numpy as np

class PCMEncoding(IntEnum):
    # ...
	def _clamp(self, samples):
        return np.clip(samples, self.min, self.max)
        
    def encode(self, amplitudes):
        match self:
            case PCMEncoding.UNSIGNED_8:
                samples = np.round((amplitudes + 1) / 2 * self.max)
                return self._clamp(samples).astype("u1").tobytes()
            case PCMEncoding.SIGNED_16:
                samples = np.round(-self.min * amplitudes)
                return self._clamp(samples).astype("<i2").tobytes()
            case PCMEncoding.SIGNED_24:
                samples = np.round(-self.min * amplitudes)
                return (
                    self._clamp(samples)
                    .astype("<i4")
                    .view("u1")
                    .reshape(-1, 4)[:, :3]
                    .flatten()
                    .tobytes()
                )
            case PCMEncoding.SIGNED_32:
                samples = np.round(-self.min * amplitudes)
                return self._clamp(samples).astype("<i4").tobytes()
            case _:
                raise TypeError("unsupported encoding")

封装 WAV 文件的元数据

管理WAV文件的多个元数据可能很麻烦,因此我们自定义一个数据类,将它们分组在一个命名空间下。
waveio/metadata.py

from dataclasses import dataclass

from waveio.encoding import PCMEncoding

@dataclass(frozen=True)
class WAVMetadata:
    encoding: PCMEncoding
    frames_per_second: float
    num_channels: int
    num_frames: int | None = None

考虑到人类认喜欢用秒表示声音持续时间,我们添加一个属性num_seconds进行帧–>秒的转换:

@dataclass(frozen=True)
class WAVMetadata:
    ...

    @property
    def num_seconds(self):
        if self.num_frames is None:
            raise ValueError("indeterminate stream of audio frames")
        return self.num_frames / self.frames_per_second

加载所有音频帧

使用原始的wave读取wav文件需要手动处理二进制数据,我们将创建reader 避免这一麻烦。

waveio/reader.py

import wave

from waveio.encoding import PCMEncoding
from waveio.metadata import WAVMetadata

class WAVReader:
    def __init__(self, path):
        self._wav_file = wave.open(str(path))
        self.metadata = WAVMetadata(
            PCMEncoding(self._wav_file.getsampwidth()),
            self._wav_file.getframerate(),
            self._wav_file.getnchannels(),
            self._wav_file.getnframes(),
        )

    def __enter__(self):
        return self

    def __exit__(self, *args, **kwargs):
        self._wav_file.close()

对于较小的文件,可以直接加载到内存:

class WAVReader:
    # ...

    def _read(self, max_frames=None):
        self._wav_file.rewind()
        frames = self._wav_file.readframes(max_frames)
        return self.metadata.encoding.decode(frames)

readframes()会向前移动文件指针,rewind()会将指针重置在开头,确保每次读取都是从头开始读取。

但是,在处理音频信号时,通常需要将数据视为帧/通道序列,而不是单个幅度样本。幸运的是,根据您的需要,您可以快速将一维 NumPy 数组重塑为合适的二维帧或通道矩阵。

我们将通过reshape装饰器实现这一功能。

import wave
from functools import cached_property

from waveio.encoding import PCMEncoding
from waveio.metadata import WAVMetadata

class WAVReader:
    # ...

    @cached_property
    @reshape("rows")
    def frames(self):
        return self._read(self.metadata.num_frames)

    @cached_property
    @reshape("columns")
    def channels(self):
        return self.frames

reshape装饰器的实现如下:

import wave
from functools import cached_property, wraps

from waveio.encoding import PCMEncoding
from waveio.metadata import WAVMetadata

def reshape(shape):
    if shape not in ("rows", "columns"):
        raise ValueError("shape must be either 'rows' or 'columns'")

    def decorator(method):
        @wraps(method)
        def wrapper(self, *args, **kwargs):
            values = method(self, *args, **kwargs)
            reshaped = values.reshape(-1, self.metadata.num_channels)
            return reshaped if shape == "rows" else reshaped.T
        return wrapper

    return decorator

# ...

为了让WAVReader在外部可用,我们在waveio.__init__.py中暴漏WAVReader类:

from waveio.reader import WAVReader

__all__ = ["WAVReader"]

使用 Matplotlib 绘制静态波形

我们已经可以进行wav文件的读取了,一个很直接的应用是使用matplotlib绘制声音的波形。
在这里插入图片描述

plot_waveform.py

from argparse import ArgumentParser
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FuncFormatter
from waveio import WAVReader

def main():
    args = parse_args()
    with WAVReader(args.path) as wav:
        plot(args.path.name, wav.metadata, wav.channels)

def parse_args():
    parser = ArgumentParser(description="Plot the waveform of a WAV file")
    parser.add_argument("path", type=Path, help="path to the WAV file")
    return parser.parse_args()

def plot(filename, metadata, channels):
    fig, ax = plt.subplots(
        nrows=metadata.num_channels,
        ncols=1,
        figsize=(16, 9),
        sharex=True, # 共享x轴
    )

    if isinstance(ax, plt.Axes):
        ax = [ax]

    time_formatter = FuncFormatter(format_time)
    timeline = np.linspace(
        start=0,
        stop=metadata.num_seconds,
        num=metadata.num_frames
    )

    for i, channel in enumerate(channels):
        ax[i].set_title(f"Channel #{i + 1}")
        ax[i].set_yticks([-1, -0.5, 0, 0.5, 1])
        ax[i].xaxis.set_major_formatter(time_formatter)
        ax[i].plot(timeline, channel)

    fig.canvas.manager.set_window_title(filename)
    plt.tight_layout()
    plt.show()

def format_time(instant, _):
    if instant < 60:
        return f"{instant:g}s"
    minutes, seconds = divmod(instant, 60)
    return f"{minutes:g}m {seconds:02g}s"

if __name__ == "__main__":
    main()

执行

python .\plot_waveform.py .\sounds\Bicycle-bell.wav

可以看到上面的波形图。

读取音频帧的切片

如果您有一个特别长的音频文件,则可以通过缩小感兴趣的音频帧的范围来减少加载和解码基础数据所需的时间。
我们将通过切片功能实现读取一个范围的音频
在这里插入图片描述

首先在脚本参数中添加起始点(start)和结束点(end)这两个参数。

# ...

def parse_args():
    parser = ArgumentParser(description="Plot the waveform of a WAV file")
    parser.add_argument("path", type=Path, help="path to the WAV file")
    parser.add_argument(
        "-s",
        "--start",
        type=float,
        default=0.0,
        help="start time in seconds (default: 0.0)",
    )
    parser.add_argument(
        "-e",
        "--end",
        type=float,
        default=None,
        help="end time in seconds (default: end of file)",
    )
    return parser.parse_args()
    
def main():
    args = parse_args()
    with WAVReader(args.path) as wav:
        plot(
            args.path.name,
            wav.metadata,
            wav.channels_sliced(args.start, args.end)
        )

# ...

plot中,时间轴不再从0开始,需要和切片时间匹配:

# ...

def plot(filename, metadata, channels):
    # ...

    time_formatter = FuncFormatter(format_time)
    timeline = np.linspace(
        channels.frames_range.start / metadata.frames_per_second,
        channels.frames_range.stop / metadata.frames_per_second,
        len(channels.frames_range)
    )

然后我们需要更新reader.py文件,读取音频的任意切片

# ...

class WAVReader:
    # ...

    @cached_property
    @reshape("rows")
    def frames(self):
        return self._read(self.metadata.num_frames, start_frame=0)

    # ...

    def _read(self, max_frames=None, start_frame=None):
        if start_frame is not None:
            self._wav_file.setpos(start_frame) # 设置起始位置
        frames = self._wav_file.readframes(max_frames)
        return self.metadata.encoding.decode(frames)


    @reshape("columns")
    def channels_sliced(self, start_seconds=0.0, end_seconds=None):
        if end_seconds is None:
            end_seconds = self.metadata.num_seconds
        frames_slice = slice(
            round(self.metadata.frames_per_second * start_seconds),
            round(self.metadata.frames_per_second * end_seconds)
        )
        frames_range = range(*frames_slice.indices(self.metadata.num_frames))
        values = self._read(len(frames_range), frames_range.start)
        return ArraySlice(values, frames_range)

我们借助了ArraySlice包装切片,包装了numpy array并且公开了便于绘制时间线的.frames_rage属性。
reader.py中添加ArraySlice的定义:

# ...

class ArraySlice:
    def __init__(self, values, frames_range):
        self.values = values
        self.frames_range = frames_range

    def __iter__(self):
        return iter(self.values)

    def __getattr__(self, name):
        return getattr(self.values, name)

    def reshape(self, *args, **kwargs):
        reshaped = self.values.reshape(*args, **kwargs)
        return ArraySlice(reshaped, self.frames_range)

    @property
    def T(self):
        return ArraySlice(self.values.T, self.frames_range)

# ...

现在,您可以通过提供 --start 和 --end 参数来放大所有通道中的特定音频帧片段

python plot_waveform.py Bongo_sound.wav --start 3.5 --end 3.65

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1577436.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在git上先新建仓库-把本地文件提交远程

一.在git新建远程项目库 1.选择新建仓库 以下以gitee为例 2.输入仓库名称&#xff0c;点击创建 这个可以选择仓库私有化还公开权限 3.获取仓库clone链接 这里选择https模式就行&#xff0c;就不需要配置对电脑进行sshkey配置了。只是需要每次提交输入账号密码 二、远…

解决网站“不安全”、“不受信”、“排名下降”,你需要——「SSL证书」

在网络时代&#xff0c;确保网站用户数据安全显得愈发关键。SSL证书作为网络安全的关键要素&#xff0c;对网站而言具有重大意义。 SSL&#xff08;Secure Sockets Layer&#xff09;证书是一种数字证书&#xff0c;用于加密和验证网络通信。它存在于客户端&#xff08;浏览…

【小白学机器学习12】假设检验之3:t 检验 (t检验量,t分布,查t值表等)

目录 1 t 检验的定义 1.1 来自维基百科和百度百科 1.2 别名 1.3 和其他检验的区别 2 适用情况&#xff1a; 2.1 关于样本情况 2.2 适合检查的情况 2.2.1 单样本均值检验&#xff08;One-sample t-test&#xff09; 2.2.2 两独立样本均值检验&#xff08;Independent …

【随笔】Git 高级篇 -- 提交的技巧(上) rebase commit --amend(十八)

&#x1f48c; 所属专栏&#xff1a;【Git】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大…

鸿蒙南向开发:【智能烟感】

样例简介 智能烟感系统通过实时监测环境中烟雾浓度&#xff0c;当烟雾浓度超标时&#xff0c;及时向用户发出警报。在连接网络后&#xff0c;配合数字管家应用&#xff0c;用户可以远程配置智能烟感系统的报警阈值&#xff0c;远程接收智能烟感系统报警信息。实现对危险及时报…

python 如何生成uuid

UUID&#xff08;Universally Unique Identifier&#xff09;是通用唯一识别码&#xff0c;在许多领域用作标识&#xff0c;比如我们常用的数据库也可以用它来作为主键&#xff0c;原理上它是可以对任何东西进行唯一的编码的。作为新手一看到类似varchar(40)这样的主键就觉得有…

ctf刷题记录2(更新中)

因为csdn上内容过多编辑的时候会很卡&#xff0c;因此重开一篇&#xff0c;继续刷题之旅。 NewStarCTF 2023 WEEK3 Include &#x1f350; <?phperror_reporting(0);if(isset($_GET[file])) {$file $_GET[file];if(preg_match(/flag|log|session|filter|input|data/i, $…

笔记 | 编译原理L1

重点关注过程式程序设计语言编译程序的构造原理和技术 1 程序设计语言 1.1 依据不同范型 过程式(Procedural programming languages–imperative)函数式(Functional programming languages–declarative)逻辑式(Logical programming languages–declarative)对象式(Object-or…

解决游戏霍格沃兹找不到EMP.dll问题的5种方法

在玩《霍格沃兹》游戏时&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中之一就是“缺少dll文件”。其中&#xff0c;EMP.dll文件丢失是一个常见的问题。这个问题可能会导致游戏无法正常运行或出现各种错误。为了解决这个问题&#xff0c;本文将介绍5种解决方法&#x…

【线段树】【前缀和】:1687从仓库到码头运输箱子

本题简单解法 C前缀和算法的应用&#xff1a;1687从仓库到码头运输箱子 本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 线段树 LeetCode1687从仓库到码头运输箱子 你有一辆货运卡车&#xff0c;你需要用这一辆车…

paddle实现手写数字模型(一)

参考文档&#xff1a;paddle官网文档环境&#xff1a;Python 3.12.2 &#xff0c;pip 24.0 &#xff0c;paddlepaddle 2.6.0 python -m pip install paddlepaddle2.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple调试代码如下&#xff1a; LeNet.py import paddle import p…

初学python记录:力扣1600. 王位继承顺序

题目&#xff1a; 一个王国里住着国王、他的孩子们、他的孙子们等等。每一个时间点&#xff0c;这个家庭里有人出生也有人死亡。 这个王国有一个明确规定的王位继承顺序&#xff0c;第一继承人总是国王自己。我们定义递归函数 Successor(x, curOrder) &#xff0c;给定一个人…

数据结构——二叉树——二叉搜索树(Binary Search Tree, BST)

目录 一、98. 验证二叉搜索树 二、96. 不同的二叉搜索树 三、538. 把二叉搜索树转换为累加树 二叉搜索树&#xff1a;对于二叉搜索树中的每个结点&#xff0c;其左子结点的值小于该结点的值&#xff0c;而右子结点的值大于该结点的值 一、98. 验证二叉搜索树 给你一个二叉树的…

GAMES Webinar 317-渲染专题-图形学 vs. 视觉大模型|Talk+Panel形式

两条路线&#xff1a;传统渲染路线&#xff0c;生成路线 两种路线的目的都是最终生成图片或者视频等在现在生成大火的情况下&#xff0c;传统路线未来该如何发展呢&#xff0c;两种路线是否能够兼容呢 严令琪 这篇工作是吸取这两条路各自优势的一篇工作 RGB是一张图&#xff…

好用的AI智能工具:AI写作、AI绘画、AI翻译全都有

在科技不断进步的今天&#xff0c;人工智能&#xff08;AI&#xff09;已经成为我们日常生活中不可或缺的一部分。它不仅在各个领域都有应用&#xff0c;还为我们提供了许多方便快捷的工具。对此&#xff0c;小编今天推荐7款人工智能软件&#xff0c;AI写作、AI绘画、AI翻译全都…

Vue - 你知道Vue组件之间是如何进行数据传递的吗

难度级别:中级及以上 提问概率:85% 这道题还可以理解为Vue组件之间的数据是如何进行共享的,也可以理解为组件之间是如何通信的,很多人叫法不同,但都是说的同一个意思。我们知道,在Vue单页面应用项目中,所有的组件都是被嵌套在App.vue内…

2024/4/1—力扣—BiNode

代码实现&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* };*/void convertBiNode_pro(struct TreeNode *root, struct TreeNode **p) {if (root) {convertBiNode_pro(roo…

Git - 如何重置或更改 Git SSH 密钥的密码?

Git 使用 ssh 方式拉取代码时&#xff0c;报 ssh password login&#xff0c;提示输入密码&#xff0c;这时很容易误填为 Git 的登录密码&#xff0c;其实这时需要输入 SSH 证书的密码&#xff0c;下面直接提供更改以及重新导入证书的方式。 首先需要确认你的本地是否有 SSH 钥…

HIS系统是什么?一套前后端分离云HIS系统源码 接口技术RESTful API + WebSocket + WebService

HIS系统是什么&#xff1f;一套前后端分离云HIS系统源码 接口技术RESTful API WebSocket WebService 医院管理信息系统(全称为Hospital Information System)即HIS系统。 常规模版包括门诊管理、住院管理、药房管理、药库管理、院长查询、电子处方、物资管理、媒体管理等&…

与汇智知了堂共舞,HW行动开启你的网络安全新篇章!

**网安圈内一年一度的HW行动来啦&#xff01; ** 招募对象 不限&#xff0c;有HW项目经验 或持有NISP二级、CISP证书优先 HW时间 以官方正式通知为准 工作地点&#xff1a;全国 薪资待遇 带薪HW &#xff08;根据考核成绩500-4000元/天不等&#xff09; 招募流程 1.填写报名…