'''
Python module to read, play, and write sound data.
For flexibility, FFMPEG is used for non-WAV files.
If you re-set the location of FFMPEG, please run the following code:
>>> ffmpeg = FFMPEG_info()
>>> ffmpeg.set()
You can obtain it for free from
http://ffmpeg.org
Mac users using Anaconda should follow the instructions on
https://anaconda.org/soft-matter/ffmpeg
Otherwise, the tips under
https://github.com/fluent-ffmpeg/node-fluent-ffmpeg/wiki/Installing-ffmpeg-on-Mac-OS-X
seemed to work. Binaries are also available from
- http://www.evermeet.cx/ffmpeg/ffmpeg-3.2.4.7z
- http://www.evermeet.cx/ffplay/ffplay-3.2.4.7z
Note that FFMPEG must be installed externally!
Please install ffmpeg/ffplay in the following directory:
- Windows: "C:\\\\Program Files\\\\ffmpeg\\\\bin\\\\"
- Mac: "/usr/local/bin/" (is already included in the default paths of the Mac terminal.)
- Linux: "/usr/bin/"
Compatible with Python >=3.5
'''
'''
Date: March-2017
Ver: 0.3
Author: thomas haslwanter
'''
# "ffmpeg" has to be installed externally, into the location listed below
# You can obtain it for free from http://ffmpeg.org
import numpy as np
import os
import sys
from scipy.io.wavfile import read, write
import tempfile
import subprocess
import json
import time
import appdirs
import easygui
# The following construct is required since I want to run the module as a script
# inside the sksound-directory
PACKAGE_PARENT = '..'
SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__))
sys.path.append(os.path.normpath(os.path.join(SCRIPT_DIR, PACKAGE_PARENT)))
# On Win playing sound works automatically
# For the other packages you need the module "pygame"
if sys.platform=='win32':
import winsound
elif sys.platform == 'linux':
import pygame
class NoFFMPEG_Error(Exception):
pass
[docs]class FFMPEG_info:
'''
Class for storing the config-info for FFMPEG.
If first checks if FFMPEG is installed. FFMPEG is necessary to
read MP3-files etc. Once checked, the corresponding information
is saved in "ffmpeg.json", under "/FFMPEG_info/sksound/":
FFMPEG_info properties:
- config_file : JSON-file, with the config-information
- ffmpeg : Commandline location of the command "ffmpeg"
- ffplay : Commandline location of the command "ffplay"
'''
def __init__(self):
"""Set the name of the config-file, and the properties
"ffmpeg" and "ffplay" of the FFMPEG_info object"""
app_name = 'FFMPEG_info'
app_author = 'sksound'
# The package "appdirs" allows an OS-independent implementation
user_data_dir = appdirs.user_data_dir(app_name, app_author)
if not os.path.exists(user_data_dir):
os.makedirs(user_data_dir)
self.config_file = os.path.join(user_data_dir, 'ffmpeg.json')
if not os.path.exists(self.config_file):
# Check if it is in the system path
try:
completed_process = subprocess.run('ffmpeg')
completed_process = subprocess.run('ffplay')
self.ffmpeg = 'ffmpeg'
self.ffplay = 'ffplay'
except FileNotFoundError:
self.set()
else:
with open(self.config_file, 'r') as in_file:
info = json.load(in_file)
self.ffmpeg = info['ffmpeg']
self.ffplay = info['ffplay']
[docs] def set(self):
"""
Set the config-filename, and write the FFMPEG_info
properties "ffmpeg" and "ffplay" to that config-file.
If FFMPEG is not installed, these are set to "None".
"""
ffmpeg_installed = easygui.ynbox(msg="Is FFMPEG installed?")
if ffmpeg_installed:
ffmpeg_dir = easygui.diropenbox(msg='Please select the directory where FFMPEG (binary) is installed:')
if sys.platform=='win32':
self.ffmpeg = os.path.join(ffmpeg_dir, 'ffmpeg.exe')
self.ffplay = os.path.join(ffmpeg_dir, 'ffplay.exe')
else:
self.ffmpeg = os.path.join(ffmpeg_dir, 'ffmpeg')
self.ffplay = os.path.join(ffmpeg_dir, 'ffplay')
if not os.path.exists(self.ffmpeg):
print('Sorry, {0} does not exist!'.format(self.ffmpeg))
return
if not os.path.exists(self.ffplay):
print('Sorry, {0} does not exist!'.format(self.ffplay))
return
else:
self.ffmpeg = None
self.ffplay = None
# Save them to the default config file
info = {'ffmpeg':self.ffmpeg, 'ffplay': self.ffplay}
try:
with open(self.config_file, 'w') as outFile:
json.dump(info, outFile)
print('Config information written to {0}'.format(os.path.abspath(self.config_file)))
except PermissionError as e:
curDir = os.path.abspath(os.curdir)
print('Current directory: {0}'.format(curDir))
print('Error: {0}'.format(e))
return
[docs]class Sound:
'''
Class for working with sound in Python.
A Sound object can be initialized
- by giving a filename
- by providing "int16" data and a rate
- without giving any parameter; in that case the user is prompted
to select an infile
Parameters
----------
inFile : string
path- and file-name of infile, if you get the sound from a file.
inData: array
manually generated sound data; requires "inRate" to be set, too.
inRate: integer
sample rate; required if "inData" are entered.
Returns
-------
None :
No return value. Initializes the Sound-properties.
Notes
-----
For non WAV-files, the file is first converted to WAV using
FFMPEG, and then read in. A warning is generated, to avoid
unintentional deletion of existing WAV-files.
SoundProperties:
- source
- data
- rate
- numChannels
- totalSamples
- duration
- bitsPerSample
Examples
--------
>>> from sksound.sounds import Sound
>>> mySound1 = Sound()
>>> mySound2 = Sound('test.wav')
>>>
>>> rate = 22050
>>> dt = 1./rate
>>> freq = 440
>>> t = np.arange(0,0.5,dt)
>>> x = np.sin(2*np.pi*freq * t)
>>> amp = 2**13
>>> sounddata = np.int16(x*amp)
>>> mySound3 = Sound(inData=sounddata, inRate=rate)
'''
def __init__(self, inFile = None, inData = None, inRate = None):
'''Initialize a Sound object '''
# Information about FFMPEG
self.info = FFMPEG_info()
if inData is not None:
if inRate is None:
print('Set the "rate" to the default value (8012 Hz).')
rate = 8012.0
self.generate_sound(inData, inRate)
else:
if inFile is None:
inFile = self._selectInput()
if inFile == 0:
return
try:
self.source = inFile
self.read_sound(self.source)
except FileNotFoundError as err:
print(err)
inFile = self._selectInput()
self.source = inFile
self.read_sound(self.source)
[docs] def read_sound(self, inFile):
'''
Read data from a sound-file.
Parameters
----------
inFile : string
path- and file-name of infile
Returns
-------
None :
No return value. Sets the property "data" of the object.
Notes
-----
* For non WAV-files, the file is first converted to WAV using
FFMPEG, and then read in.
* If FFMPEG is not installed, non-WAV files produce a "sounds.NoFFMPEG_Error"
Examples
--------
>>> mySound = Sound()
>>> mySound.read_sound('test.wav')
'''
# Python can natively only read "wav" files. To be flexible, use "ffmpeg" for conversion for other formats
if not os.path.exists(inFile):
print('{0} does not exist!'.format(inFile))
raise FileNotFoundError
(root, ext) = os.path.splitext(inFile)
if ext[1:].lower() != 'wav':
if self.info.ffmpeg == None:
print('Sorry, need FFMPEG for non-WAV files!')
self.rate = None
self.data = None
raise NoFFMPEG_Error
outFile = root + '.wav'
cmd = [self.info.ffmpeg, '-i', inFile, outFile, '-y']
subprocess.run(cmd)
print('Infile converted from ' + ext + ' to ".wav"')
inFile = outFile
self.source = outFile
self.rate, self.data = read(inFile)
self._setInfo()
print('data read in!')
[docs] def play(self):
'''
Play the stored sound
Parameters
----------
None :
Returns
-------
None :
Notes
-----
On "Windows" the module "winsound" is used; on "Linux" I use
"pygame"; and on "OSX" the terminal command "afplay".
Examples
--------
>>> mySound = Sound()
>>> mySound.read_sound('test.wav')
>>> mySound.play()
'''
try:
if self.source is None:
# If there is no source-file, write the data to a temporary WAV-file ...
tmpFile = tempfile.NamedTemporaryFile(suffix='.wav', delete=False)
tmpFile.close()
self.write_wav(tmpFile.name)
# ... and play that file
if sys.platform=='win32':
winsound.PlaySound(tmpFile.name, winsound.SND_FILENAME)
elif sys.platform == 'darwin':
cmd = ['afplay', tmpFile.name]
subprocess.run(cmd)
else:
pygame.init()
pygame.mixer.music.load(tmpFile.name)
pygame.mixer.music.play()
time.sleep(self.duration)
# If you want to use FFMPEG instead, use the following commands:
#cmd = [self.info.ffplay, '-autoexit', '-nodisp', '-i', tmpFile.name]
#subprocess.run(cmd)
elif os.path.exists(self.source):
# If you have a given input file ...
print('Playing ' + self.source)
# ... then play that one
if sys.platform=='win32':
winsound.PlaySound(str(self.source), winsound.SND_FILENAME)
elif sys.platform == 'darwin':
cmd = ['afplay', str(self.source)]
subprocess.run(cmd)
else:
pygame.init()
pygame.mixer.music.load(self.source)
pygame.mixer.music.play()
time.sleep(self.duration)
# If you want to use FFMPEG instead, use the following commands:
#cmd = [self.info.ffplay, '-autoexit', '-nodisp', '-i', self.source]
#subprocess.run(cmd)
except SystemError:
print('If you don''t have FFMPEG available, you can e.g. use installed audio-files. E.g.:')
print('import subprocess')
print('subprocess.run([r"C:\Program Files (x86)\VideoLAN\VLC\vlc.exe", r"C:\Music\14_Streets_of_Philadelphia.mp3"])')
[docs] def generate_sound(self, data, rate):
''' Set the properties of a Sound-object. '''
# If the data are in another format, e.g. "float", convert
# them to integer and scale them to a reasonable amplitude
if not type(data[0]) == np.int16:
defaultAmp = 2**13
# Watch out with integer artefacts!
data = np.int16(data * (defaultAmp / np.max(data)))
self.data = data
self.rate = rate
self.source = None
self._setInfo()
[docs] def write_wav(self, full_out_file = None):
'''
Write sound data to a WAV-file.
Parameters
----------
fullOutFile : string
Path- and file-name of the outfile. If none is given,
the user is asked interactively to choose a folder/name
for the outfile.
Returns
-------
None :
Examples
--------
>>> mySound = Sound()
>>> mySound.read_sound('test.wav')
>>> mySound.write_wav()
'''
if full_out_file is None:
full_out_file = easygui.filesavebox(msg='Write sound to ....', default='*.wav',
filetypes=['*.wav'])
if full_out_file is None:
print('Output discarded.')
return 0
else:
(outFile , outDir) = os.path.split(full_out_file)
write(str(full_out_file), int(self.rate), self.data)
print('Sounddata written to ' + outFile + ', with a sample rate of ' + str(self.rate))
print('OutDir: ' + outDir)
return full_out_file
[docs] def get_info(self):
'''
Return information about the sound.
Parameters
----------
None :
Returns
-------
source : name of inFile
rate : sampleRate
numChannels : number of channels
totalSamples : number of total samples
duration : duration [sec]
bitsPerSample : bits per sample
Examples
--------
>>> mySound = Sound()
>>> mySound.read_sound('test.wav')
>>> info = mySound.get_info()
>>> (source, rate, numChannels, totalSamples, duration, bitsPerSample) = mySound.info()
'''
return (self.source,
self.rate,
self.numChannels,
self.totalSamples,
self.duration,
self.dataType)
[docs] def summary(self):
'''
Display information about the sound.
Parameters
----------
None :
Returns
-------
None :
Examples
--------
>>> mySound = Sound()
>>> mySound.read_sound('test.wav')
>>> mySound.summary()
'''
import yaml
(source, rate, numChannels, totalSamples, duration, dataType) = self.get_info()
info = {'Source':source,
'SampleRate':rate,
'NumChannels':numChannels,
'TotalSamples':totalSamples,
'Duration':duration,
'DataType':dataType}
print(yaml.dump(info, default_flow_style=False))
def _setInfo(self):
'''Set the information properties of that sound'''
if len(self.data.shape)==1:
self.numChannels = 1
self.totalSamples = len(self.data)
else:
self.numChannels = self.data.shape[1]
self.totalSamples = self.data.shape[0]
self.duration = float(self.totalSamples)/self.rate # [sec]
self.dataType = str(self.data.dtype)
def _selectInput(self):
'''GUI for the selection of an in-file. '''
full_in_file = easygui.fileopenbox(msg='Select sound-input:', default='*.wav',
filetypes=['*.wav', '*.mp3'])
if full_in_file is None:
print('No file selected')
return 0
else:
print('Selection: ' + full_in_file)
return full_in_file
[docs]def main():
''' Main function, to test the module '''
import os
import numpy as np
import matplotlib.pyplot as plt
# Re-set FFMPEG
#ffmpeg = FFMPEG_info()
#ffmpeg.set()
### Import a file, and play the sound
dataDir = r'/home/thomas/Coding/scikit-sound/sksound/tests'
inFile = 'YouAreNotIt.mp3'
fullFile = os.path.join(dataDir, inFile)
try:
mySound = Sound(fullFile)
mySound.play()
time.sleep(mySound.duration)
except NoFFMPEG_Error:
pass
## Test with self-generated data
rate = 22050
dt = 1./rate
t = np.arange(0,0.5,dt)
freq = 880
x = np.sin(2*np.pi*freq*t)
sounddata = np.int16(x*2**13)
inSound = Sound(inData=sounddata, inRate=rate)
inSound.summary()
inSound.play()
time.sleep(inSound.duration)
print('hi')
## Test if type conversion works
#inSound2 = Sound(inData=x, inRate=rate)
#inSound2.play()
# Test with GUI
#inSound = Sound()
#inSound.play()
#print(inSound.summary())
#out = inSound.get_info()
#print(out)
#inSound.write_wav()
if __name__ == '__main__':
main()