Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added pitch shifting to audio_sdl2 #4372

Merged
merged 4 commits into from Aug 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 42 additions & 0 deletions examples/audio/pitch.py
@@ -0,0 +1,42 @@
# encoding: utf8

from kivy.app import App
from kivy.lang import Builder
from kivy.clock import Clock
from kivy.core.audio import SoundLoader
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.button import Button

from sys import version_info


NOTES = (
('Do', 1),
('Ré', 9/8.),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably just be Re

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe just A, B, C…

('Mi', 5/4.),
('Fa', 4/3.),
('Sol', 3/2.),
('La', 5/3.),
('Si', 15/8.),
)

class Test(App):
def build(self):
self.sound = SoundLoader.load(
'/usr/lib64/python{}.{}/test/audiodata/pluck-pcm32.wav'
.format(*version_info[0:2])
)
root = BoxLayout()
for octave in range(-2, 3):
for note, pitch in NOTES:
button = Button(text=note)
button.pitch = pitch * 2 ** octave
button.bind(on_release=self.play_note)
root.add_widget(button)
return root

def play_note(self, button):
self.sound.pitch = button.pitch
self.sound.play()

Test().run()
14 changes: 13 additions & 1 deletion kivy/core/audio/__init__.py
Expand Up @@ -50,10 +50,12 @@
from kivy.compat import PY2
from kivy.resources import resource_find
from kivy.properties import StringProperty, NumericProperty, OptionProperty, \
AliasProperty, BooleanProperty
AliasProperty, BooleanProperty, BoundedNumericProperty
from kivy.utils import platform
from kivy.setupconfig import USE_SDL2

from sys import float_info


class SoundLoader:
'''Load a sound, using the best loader for the given file type.
Expand Down Expand Up @@ -116,6 +118,16 @@ class Sound(EventDispatcher):
to 1.
'''

pitch = BoundedNumericProperty(1., min=float_info.epsilon)
'''Pitch of a sound. 2 is an octave higher, .5 one below. This is only
implemented for SDL2 audio provider yet.

.. versionadded:: 1.9.2

:attr:`pitch` is a :class:`~kivy.properties.NumericProperty` and defaults
to 1.
'''

state = OptionProperty('stop', options=('stop', 'play'))
'''State of the sound, one of 'stop' or 'play'.

Expand Down
35 changes: 28 additions & 7 deletions kivy/core/audio/audio_sdl2.pyx
Expand Up @@ -28,6 +28,7 @@ Depending the compilation of SDL2 mixer and/or installed libraries:
__all__ = ('SoundSDL2', 'MusicSDL2')

include "../../../kivy/lib/sdl2.pxi"
include "../../../kivy/graphics/common.pxi" # For malloc and memcpy (on_pitch)

from kivy.core.audio import Sound, SoundLoader
from kivy.logger import Logger
Expand All @@ -36,11 +37,6 @@ from kivy.clock import Clock
cdef int mix_is_init = 0
cdef int mix_flags = 0

# old code from audio_sdl, never used it = unfinished?
#cdef void channel_finished_cb(int channel) nogil:
# with gil:
# print('Channel finished playing.', channel)


cdef mix_init():
cdef int audio_rate = 44100
Expand Down Expand Up @@ -72,14 +68,13 @@ cdef mix_init():
mix_is_init = -1
return 0

#Mix_ChannelFinished(channel_finished_cb)

mix_is_init = 1
return 1

# Container for samples (Mix_LoadWAV)
cdef class ChunkContainer:
cdef Mix_Chunk *chunk
cdef Mix_Chunk *original_chunk
cdef int channel

def __init__(self):
Expand All @@ -92,6 +87,9 @@ cdef class ChunkContainer:
Mix_HaltChannel(self.channel)
Mix_FreeChunk(self.chunk)
self.chunk = NULL
if self.original_chunk != NULL:
Mix_FreeChunk(self.original_chunk)
self.original_chunk = NULL

# Container for music (Mix_LoadMUS), one channel only
cdef class MusicContainer:
Expand Down Expand Up @@ -158,6 +156,26 @@ class SoundSDL2(Sound):
frames = points / channels
return <double>frames / <double>freq

def on_pitch(self, instance, value):
cdef ChunkContainer cc = self.cc
cdef int freq, channels
cdef unsigned short fmt
cdef SDL_AudioCVT cvt
if cc.chunk == NULL:
return
if not Mix_QuerySpec(&freq, &fmt, &channels):
return
SDL_BuildAudioCVT(
&cvt,
fmt, channels, int(freq * self.pitch),
fmt, channels, freq,
)
cvt.buf = <Uint8 *>malloc(cc.original_chunk.alen * cvt.len_mult)
cvt.len = cc.original_chunk.alen
memcpy(cvt.buf, cc.original_chunk.abuf, cc.original_chunk.alen)
SDL_ConvertAudio(&cvt)
cc.chunk = Mix_QuickLoad_RAW(cvt.buf, <Uint32>(cvt.len * cvt.len_ratio))

def play(self):
cdef ChunkContainer cc = self.cc
self.stop()
Expand Down Expand Up @@ -201,7 +219,10 @@ class SoundSDL2(Sound):
Logger.warning('AudioSDL2: Unable to load {}: {}'.format(
self.filename, Mix_GetError()))
else:
cc.original_chunk = Mix_QuickLoad_RAW(cc.chunk.abuf, cc.chunk.alen)
cc.chunk.volume = int(self.volume * 128)
if self.pitch != 1.:
self.on_pitch(self, self.pitch)

def unload(self):
cdef ChunkContainer cc = self.cc
Expand Down
24 changes: 24 additions & 0 deletions kivy/lib/sdl2.pxi
Expand Up @@ -775,6 +775,30 @@ cdef extern from "SDL_ttf.h":

cdef extern from "SDL_audio.h":
cdef int AUDIO_S16SYS
ctypedef struct SDL_AudioFilter:
pass
ctypedef struct SDL_AudioCVT:
int needed
int src_format
int dst_format
double rate_incr
Uint8 *buf
int len
int len_cvt
int len_mult
double len_ratio
SDL_AudioFilter filters[10]
int filter_index
cdef int SDL_BuildAudioCVT(
SDL_AudioCVT *cvt,
int src_format,
Uint8 src_channels,
int src_rate,
int dst_format,
Uint8 dst_channels,
int dst_rate
)
cdef int SDL_ConvertAudio(SDL_AudioCVT *cvt)

cdef extern from "SDL_mixer.h":
cdef struct Mix_Chunk:
Expand Down