首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >Python程序对MP3音频进行置乱

Python程序对MP3音频进行置乱
EN

Code Review用户
提问于 2022-08-16 02:43:25
回答 4查看 3.7K关注 0票数 17

我对编码很陌生。我在车里有一个随机的想法来制作这个应用程序,以便首先深入学习代码。

这是一个应用程序,获取一个MP3文件,将其“分解”成块(临时创建1秒mp3文件),然后随机化这些片段,然后以随机顺序编译一个包含所有“碎片”的新MP3文件。为了完成这个任务,我使用了我有限的知识,所以我的代码可能写得不好。

关于我的代码你有什么可以改变的吗?任何批评都会有帮助。我在大约10个小时内学会并组装了这些东西。(我真的很享受!)

我的未来目标是让这个功能可以通过网络访问,这样我的朋友们就可以不用下载可执行代码就可以使用它。

代码语言:javascript
复制
from pydub import AudioSegment
from mutagen.mp3 import MP3
import random
import os
from glob import glob
from string import ascii_lowercase
from random import choice, randint, random
import sys
import time
from PyQt5.QtWidgets import * 
from PyQt5.QtGui import * 
from PyQt5.QtCore import *
from PyQt5 import QtCore
from PyQt5 import QtWidgets

class shredder(QWidget):
   def __init__(self, parent = None):
      super(shredder, self).__init__(parent)

      self.setMinimumSize(350, 100)
      #self.setFixedWidth(350)
      #self.setFixedHeight(100)

      layout = QVBoxLayout()
      self.btn = QPushButton("Load MP3")
      self.btn.clicked.connect(self.getfile)

      layout.addWidget(self.btn)
      self.le = QLabel("File Location:")
      self.le.setAlignment(QtCore.Qt.AlignCenter)

      layout.addWidget(self.le)
      self.btn1 = QPushButton("Shred")
      self.btn1.clicked.connect(self.shred)
      layout.addWidget(self.btn1)
            
      self.setLayout(layout)
      self.setWindowTitle("MP3Shredder")
    

   def getfile(self):
      global filepath
      filepath, _filter = QFileDialog.getOpenFileName(self, 'Open MP3 File', 
         '',"MP3 (*.mp3)")
      if filepath:
            self.le.setText("File Location: "+filepath)      


   def shred(self):
        global filepath
        
        try:
            filepath
        except NameError:
            self.le.setText("You Must First Load A MP3 File To Shred!") 
        else:
            #importing file with pydub from location by giving its path
            song = AudioSegment.from_mp3(filepath)
            #creates an MP3 object with mutagen from file location and saves it as variable
            audio = MP3(filepath)
            audio_info = audio.info
            length = int(audio_info.length)
            hours, mins, seconds = audio_duration(length)
            print('Total Duration Of Song: {}:{}:{}'.format(hours, mins, seconds))
            print("In Seconds: " + str(audio.info.length))
            print("Creating " + str((audio.info.length//1)+1) +" shreds.")

            os.makedirs(os.path.join(r"C:\mp3shredder"), exist_ok=True)
            os.makedirs(os.path.join(r"C:\mp3shredder\temp"), exist_ok=True)

            print("Shredding...")
            
            #split sound in 1-second slices and export
            for i, chunk in enumerate(song[::1000]):
                with open(r"C:\mp3shredder\temp\piece%s.mp3" % i, "wb") as f:
                    chunk.export(f, format="mp3")
    
            path = (r"C:\mp3shredder\temp")
            files = os.listdir(path)

            for index, file in enumerate(files):
               os.rename(os.path.join(path, file), os.path.join(path, ''.join([choice(ascii_lowercase) for _ in range(randint(5, 8))])+".mp3"))
  
            print("Shredding Complete!")

            print("Gluing...")

            shredded_pieces = [AudioSegment.from_mp3(mp3_file) for mp3_file in glob(r"C:\mp3shredder\temp\*.mp3")] 

            glue_it_back = AudioSegment.empty()
            for song in shredded_pieces:
                glue_it_back += song
            
            filedir = os.path.dirname(filepath)
            filename = os.path.basename(filepath)
            
            glue_it_back.export(filedir + "/" + "SHREDDED_" + filename, format="mp3")
            
            dir = (r"C:\mp3shredder\temp")
            for f in os.listdir(dir):
                os.remove(os.path.join(dir, f))
    
            os.rmdir(dir)
    
            print("Gluing Complete!")
            print("Created: " + filedir + "/" + "SHREDDED_" + filename)
            
            self.le.setText("Created: " + filedir + "/" + "SHREDDED_" + filename)
            os.startfile(filedir)

# function to convert the information into 
# some readable format
def audio_duration(length):
    hours = length // 3600  # calculate in hours
    length %= 3600
    mins = length // 60  # calculate in minutes
    length %= 60
    seconds = length  # calculate in seconds
    
    return hours, mins, seconds  # returns the duration
   

def main():
   app = QApplication(sys.argv)
   ex = shredder()
   ex.show()
   sys.exit(app.exec_())

if __name__ == '__main__':
   main()
EN

回答 4

Code Review用户

发布于 2022-08-16 10:37:51

  • 考虑一下,如果用户关闭"Load MP3“对话框而不选择文件,会发生什么情况。在这种情况下,filepath将被设置为""。标签不会更改,但是如果用户按下“碎片”按钮,我们将尝试导入一个名为""的文件(因为存在filepath变量,所以我们没有得到该NameError),该文件可能不会运行得很好
  • 我不知道我们为什么要把这些块写到临时文件中,然后再读一遍?似乎做这样的事情会更简单:
代码语言:javascript
复制
glued_song = AudioSegment.empty()

for chunk in random.sample(song[::1000], song.duration_seconds):
    glued_song += chunk
  • 如果您需要保存临时文件,我建议使用tempfile模块以更安全和可移植的方式在标准位置创建它。
  • 您似乎只使用mutagen来获得原始轨道的持续时间。由于pydub支持获得AudioSegments的持续时间(使用song.duration_seconds),因此只为了这个目的而承担整个其他依赖关系似乎是不必要的。
  • 将当前选定的文件路径设置为shredder类的属性可能比全局变量更整洁--您可以在__init__中像self.filepath = None一样设置它,然后在getfileshred中以self.filepath的形式访问它。
  • 我确实认为允许用户选择输出文件名可能是一个可取的特性。
  • 最后,我不太喜欢这里使用的一些变量名称:
    • shredder作为类名,通常是像Shredder那样编写的。
    • btnbtn1很少告诉我这些按钮是什么。例如,load_buttonshred_button将更具描述性。
    • 同样,我也不知道le是什么意思。我可能会选择类似path_labelselection_label之类的东西
    • getfile是两个词-- get_file。或者可能是select_file,或者pick_file,以更清楚地表示正在做出选择。
    • song在相同的方法中被用于不同的目的,这可能会引起混淆。
    • glue_it_back听起来不像一件事,而更像你做的事情--它是函数的好名字,但对于一个对象,我更喜欢glued_song,甚至是result
票数 20
EN

Code Review用户

发布于 2022-08-16 12:42:58

在总体架构和风格以及您使用的逻辑方面,都有很大的改进空间:

体系结构与风格

将逻辑与表示

分离

只有一个类处理逻辑(“分解”音频文件)和表示( PyQt小部件)。这使得测试变得更加困难,因为如果不构建GUI,就无法从测试运行程序调用shred,并且查看比应该更痛苦的内容:我不愿意仅仅为了检查而安装一个250+ MiB包(在本例中是PyQt5)。

在您的示例中,shred可以是一个独立的方法,以一个AudioFragment作为参数并返回另一个AudioFragment

此外,在处理音频文件时,您会将大量内容打印到控制台,不仅要混合逻辑和表示,还要混合多种形式的表示-- GUI和控制台。我知道它在开发和调试时可能很有用,但是您应该考虑使用记录器。

不要使用globals

filepath使用全局变量。全球通常被认为是不好的做法,虽然他们可以有合法的用途,但这不是一个。它目前可能有效,但这是一种“脚枪”:您可以从代码中的任何其他地方更改文件路径,包括GUI类之外的文件路径,并使GUI和内部状态不同步,从而为用户带来问题。考虑到泛型变量名,这一点尤其正确。

在这种情况下,可以使用实例参数(self.filepath)跟踪路径,也可以将其作为参数传递给方法。

命名

使用有意义的变量名。您通常是这样做的,但是UI小部件的名称不包含任何信息,例如,请参见btnbtn1load_file_buttonshred_button将是描述性更强的名称。

想象一下,在稍微复杂一点的UI上工作,您可能会丢失小部件在当前命名过程中快速完成的任务。

Documentation

您的代码不包含任何文档。Docstring对于理解程序所做的工作非常有用,特别是对于shred方法,该方法对您的程序非常独特。

逻辑

如果可以避免,

不要写到磁盘上,

在您的情况下,没有理由将歌曲块写入磁盘。相反,您可以将歌曲分割成块,并将它们放入数组中,用random.shuffle对它们进行洗牌,并将它们拼接到一个新的AudioFragment中。

副作用

如果用户在一个不幸名为C:\mp3shedder\temp的目录中有一些个人数据,该怎么办?在运行你的程序时它会被核爆。

如果您确实必须将其写入磁盘并从磁盘中删除(在您的情况下,您不需要这样做),那么您可能应该更加小心您的操作方式。

绝对路径上的

相对

您使用硬编码的绝对路径,这更可能在读取、写入和删除磁盘时带来副作用。

票数 16
EN

Code Review用户

发布于 2022-08-16 15:13:40

首先,对于初学者来说,这是一个非常有趣的项目,做得很好。其他人提到了很多关于我们的代码风格的问题,但是要在以下几点上进行扩展:

通用PEP-8

佩普-8是Python的标准样式推荐。其中包括空行数、缩进数、变量命名建议和大小写等。

有一些指针(Flake8皮林特和其他)可以根据PEP-8检查您的代码并发出有用的警告。在你的例子中,有几件简单而明显的事情。

白色空间

PEP-8推荐缩进的4个空格的倍数,顶层函数/类之间的2空行,内部函数之间的1空行。

操作符被空格包围。

代码语言:javascript
复制
x = a + b

关键字参数不被空格包围。

代码语言:javascript
复制
def func(a, b=None):

逗号后面跟着空格。

代码语言:javascript
复制
('A', 'B', 'C')

进口

导入random两次,一次作为普通库

代码语言:javascript
复制
import random

以及random中的特定元素

代码语言:javascript
复制
from random import choice, randint, random

这实际上会导致random、库和random.random函数之间的名称冲突。显然不太好。

将Python标准库导入放在一般库之前也是很好的实践。

导入*通常被认为是不好的做法,因为它可能会污染您的命名空间,从而使您调用函数,这是您没有预料到的。

想象一下你这样做的情况

代码语言:javascript
复制
from numpy import *
from random import *

numpyrandom都提供了一个名为random的函数,这意味着当您调用random()时可能不知道正在调用的是哪个

相反,将库导入命名空间或只导入您想要使用的内容(指针将告诉您未使用的导入)。就您的情况而言,您要导入:

  • 时间
  • random.random
  • PyQt5.QtWidgets

在您的代码中没有任何一个显式使用。

Case

PEP-8建议将UPPER_SNAKE用于顶级/全局变量,PascalCase用于类,lower_snake用于变量和函数。

shredderfilepath和其他人不符合这一点。

任务分离

您有一个名为shredder的对象,它不仅仅是一个分解器,它是执行分解的GUI。我们应该将它分开,并允许它与可能包含Shredder的GUI分开存在。

您还应该有一些函数,它们被命名来表示它们所做的事情。例如,audio_duration实际上并不关心audio,它只是将一个时间(以秒为单位)转换为小时、分钟、秒。您可以这样做两件事之一,要么将函数重命名为split_time,或者使用类似的名称来描述它实际做的事情(用docstring解释它所做的事情),或者让它按您说的那样做。

代码语言:javascript
复制
def audio_duration(audio: MP3):
    "Gets the duration of an audio track in hours, minutes and seconds"
    length = audio.info.length
    hours = length // 3600  # calculate in hours
    length %= 3600
    mins = length // 60  # calculate in minutes
    length %= 60
    seconds = length  # calculate in seconds

    return hours, mins, seconds  # returns the duration

(注:存在一个divmod函数来执行此操作)

注释、文档字符串和类型提示

注释和文档字符串是python中帮助其他人理解正在发生的事情的有用工具,然而,好的注释并不容易编写。您的一些评论只描述了代码已经告诉我的内容:

代码语言:javascript
复制
    hours = length // 3600  # calculate in hours

诚实地说,这是相当不言自明的,但是说它“以小时计算”的评论是因为我们将它分配给一个名为时数的变量。评论没有告诉我的是我们是从什么算出来的。一个更好的评论(人们会不同意,这都是关于品味的问题)可能是解释你实际上在做什么计算。

代码语言:javascript
复制
    hours = length // 3600  # Seconds to hours

同样,docstring是其他人使用您的软件的主要工具。你打过字吗

代码语言:javascript
复制
>>> import random
>>> help(random.randint)

help返回docstring,并告诉您函数应该做什么、需要什么参数、返回什么以及在某些情况下如何达到结果。良好的文档字符串在代码中也很有用,可以提醒自己函数正在做什么。同样,一个好的docstring很难编写,但是像sphinx这样的工具有一些标准格式,可以帮助指导您如何编写一个好的,以及能够自动为您的代码生成API文档(而不必手工编写)。一个简单的docstring可能看起来像

代码语言:javascript
复制
def audio_duration(length):
    """Converts duration in seconds to hours, minutes, seconds

    :param length: duration in seconds
    :returns: duration in hours, minutes, seconds
    :rtype: 3-tuple of ints

    """

虽然类型提示在Python中是可选的,但它们只是工具箱中的另一个工具,可以帮助解释您的代码正在做什么,并且常常有助于您自己和其他人查看您的代码正在做什么。

字符串格式

您正在使用旧的字符串格式(%格式和.formats)的混合样式,现代风格是使用f-字符串来明确代码中正在发生的事情。

代码语言:javascript
复制
print('Total Duration Of Song: {}:{}:{}'.format(hours, mins, seconds))
print("In Seconds: " + str(audio.info.length))
print("Creating " + str((audio.info.length//1)+1) + " shreds.")
with open(r"C:\mp3shredder\temp\piece%s.mp3" % i, "wb") as f:

成为

代码语言:javascript
复制
print(f'Total Duration Of Song: {hours}:{mins}:{seconds}')
print(f"In Seconds: {audio.info.length}")
print(f"Creating {audio.info.length//1)+1} shreds.")
with open(rf"C:\mp3shredder\temp\piece{i}.mp3", "wb") as f:

Globals

避免使用全局值,它们会导致许多问题,特别是在较大的代码中。如果您使用了一个具有公共名称的全局(如filepath ),而其他人也在他们的库中,会发生什么情况呢?谁知道会发生什么?到处都是问题。

相反,只要把事情保持在可行的地方范围内就行了。将参数传递到函数中并在本地使用。那么,对该参数的更改不会影响全局状态(情况并不总是如此,请注意您更改了虚拟参数),它可以从任何名称传入。假设我有100个想要shred的文件:

代码语言:javascript
复制
global filename
for my_file in files_list:
    filename = my_file
    Shredder.shred()

代码语言:javascript
复制
for my_file in filelist:
    Shredder.shred(my_file)

这就使得意图更明显了?

我认为这在语法和结构方面已经足够了。

票数 11
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/278877

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档