diff --git a/Lib/test/audiodata/pluck-alaw.aifc b/Lib/test/audiodata/pluck-alaw.aifc deleted file mode 100644 index 3b7fbd2af..000000000 Binary files a/Lib/test/audiodata/pluck-alaw.aifc and /dev/null differ diff --git a/Lib/test/audiodata/pluck-pcm16.aiff b/Lib/test/audiodata/pluck-pcm16.aiff deleted file mode 100644 index 6c8c40d14..000000000 Binary files a/Lib/test/audiodata/pluck-pcm16.aiff and /dev/null differ diff --git a/Lib/test/audiodata/pluck-pcm16.au b/Lib/test/audiodata/pluck-pcm16.au deleted file mode 100644 index 398f07f07..000000000 Binary files a/Lib/test/audiodata/pluck-pcm16.au and /dev/null differ diff --git a/Lib/test/audiodata/pluck-pcm24.aiff b/Lib/test/audiodata/pluck-pcm24.aiff deleted file mode 100644 index 8eba145a4..000000000 Binary files a/Lib/test/audiodata/pluck-pcm24.aiff and /dev/null differ diff --git a/Lib/test/audiodata/pluck-pcm24.au b/Lib/test/audiodata/pluck-pcm24.au deleted file mode 100644 index 0bb230418..000000000 Binary files a/Lib/test/audiodata/pluck-pcm24.au and /dev/null differ diff --git a/Lib/test/audiodata/pluck-pcm32.aiff b/Lib/test/audiodata/pluck-pcm32.aiff deleted file mode 100644 index 46ac0373f..000000000 Binary files a/Lib/test/audiodata/pluck-pcm32.aiff and /dev/null differ diff --git a/Lib/test/audiodata/pluck-pcm32.au b/Lib/test/audiodata/pluck-pcm32.au deleted file mode 100644 index 92ee5965e..000000000 Binary files a/Lib/test/audiodata/pluck-pcm32.au and /dev/null differ diff --git a/Lib/test/audiodata/pluck-pcm8.aiff b/Lib/test/audiodata/pluck-pcm8.aiff deleted file mode 100644 index 5de4f3b2d..000000000 Binary files a/Lib/test/audiodata/pluck-pcm8.aiff and /dev/null differ diff --git a/Lib/test/audiodata/pluck-pcm8.au b/Lib/test/audiodata/pluck-pcm8.au deleted file mode 100644 index b7172c8f2..000000000 Binary files a/Lib/test/audiodata/pluck-pcm8.au and /dev/null differ diff --git a/Lib/test/audiodata/pluck-ulaw.aifc b/Lib/test/audiodata/pluck-ulaw.aifc deleted file mode 100644 index 3085cf097..000000000 Binary files a/Lib/test/audiodata/pluck-ulaw.aifc and /dev/null differ diff --git a/Lib/test/audiodata/pluck-ulaw.au b/Lib/test/audiodata/pluck-ulaw.au deleted file mode 100644 index 11103535c..000000000 Binary files a/Lib/test/audiodata/pluck-ulaw.au and /dev/null differ diff --git a/Lib/test/test_winsound.py b/Lib/test/test_winsound.py new file mode 100644 index 000000000..9724d830a --- /dev/null +++ b/Lib/test/test_winsound.py @@ -0,0 +1,187 @@ +# Ridiculously simple test of the winsound module for Windows. + +import functools +import os +import time +import unittest + +from test import support +from test.support import import_helper +from test.support import os_helper + + +support.requires('audio') +winsound = import_helper.import_module('winsound') + + +# Unless we actually have an ear in the room, we have no idea whether a sound +# actually plays, and it's incredibly flaky trying to figure out if a sound +# even *should* play. Instead of guessing, just call the function and assume +# it either passed or raised the RuntimeError we expect in case of failure. +def sound_func(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + ret = func(*args, **kwargs) + except RuntimeError as e: + if support.verbose: + print(func.__name__, 'failed:', e) + else: + if support.verbose: + print(func.__name__, 'returned') + return ret + return wrapper + + +safe_Beep = sound_func(winsound.Beep) +safe_MessageBeep = sound_func(winsound.MessageBeep) +safe_PlaySound = sound_func(winsound.PlaySound) + + +class BeepTest(unittest.TestCase): + + def test_errors(self): + self.assertRaises(TypeError, winsound.Beep) + self.assertRaises(ValueError, winsound.Beep, 36, 75) + self.assertRaises(ValueError, winsound.Beep, 32768, 75) + + def test_extremes(self): + safe_Beep(37, 75) + safe_Beep(32767, 75) + + def test_increasingfrequency(self): + for i in range(100, 2000, 100): + safe_Beep(i, 75) + + def test_keyword_args(self): + safe_Beep(duration=75, frequency=2000) + + +class MessageBeepTest(unittest.TestCase): + + def tearDown(self): + time.sleep(0.5) + + def test_default(self): + self.assertRaises(TypeError, winsound.MessageBeep, "bad") + self.assertRaises(TypeError, winsound.MessageBeep, 42, 42) + safe_MessageBeep() + + def test_ok(self): + safe_MessageBeep(winsound.MB_OK) + + def test_asterisk(self): + safe_MessageBeep(winsound.MB_ICONASTERISK) + + def test_exclamation(self): + safe_MessageBeep(winsound.MB_ICONEXCLAMATION) + + def test_hand(self): + safe_MessageBeep(winsound.MB_ICONHAND) + + def test_question(self): + safe_MessageBeep(winsound.MB_ICONQUESTION) + + def test_error(self): + safe_MessageBeep(winsound.MB_ICONERROR) + + def test_information(self): + safe_MessageBeep(winsound.MB_ICONINFORMATION) + + def test_stop(self): + safe_MessageBeep(winsound.MB_ICONSTOP) + + def test_warning(self): + safe_MessageBeep(winsound.MB_ICONWARNING) + + def test_keyword_args(self): + safe_MessageBeep(type=winsound.MB_OK) + + +class PlaySoundTest(unittest.TestCase): + + def test_errors(self): + self.assertRaises(TypeError, winsound.PlaySound) + self.assertRaises(TypeError, winsound.PlaySound, "bad", "bad") + self.assertRaises( + RuntimeError, + winsound.PlaySound, + "none", winsound.SND_ASYNC | winsound.SND_MEMORY + ) + self.assertRaises(TypeError, winsound.PlaySound, b"bad", 0) + self.assertRaises(TypeError, winsound.PlaySound, "bad", + winsound.SND_MEMORY) + self.assertRaises(TypeError, winsound.PlaySound, 1, 0) + # embedded null character + self.assertRaises(ValueError, winsound.PlaySound, 'bad\0', 0) + + def test_keyword_args(self): + safe_PlaySound(flags=winsound.SND_ALIAS, sound="SystemExit") + + def test_snd_memory(self): + with open(support.findfile('pluck-pcm8.wav', + subdir='audiodata'), 'rb') as f: + audio_data = f.read() + safe_PlaySound(audio_data, winsound.SND_MEMORY) + audio_data = bytearray(audio_data) + safe_PlaySound(audio_data, winsound.SND_MEMORY) + + def test_snd_filename(self): + fn = support.findfile('pluck-pcm8.wav', subdir='audiodata') + safe_PlaySound(fn, winsound.SND_FILENAME | winsound.SND_NODEFAULT) + + def test_snd_filepath(self): + fn = support.findfile('pluck-pcm8.wav', subdir='audiodata') + path = os_helper.FakePath(fn) + safe_PlaySound(path, winsound.SND_FILENAME | winsound.SND_NODEFAULT) + + def test_snd_filepath_as_bytes(self): + fn = support.findfile('pluck-pcm8.wav', subdir='audiodata') + self.assertRaises( + TypeError, + winsound.PlaySound, + os_helper.FakePath(os.fsencode(fn)), + winsound.SND_FILENAME | winsound.SND_NODEFAULT + ) + + def test_aliases(self): + aliases = [ + "SystemAsterisk", + "SystemExclamation", + "SystemExit", + "SystemHand", + "SystemQuestion", + ] + for alias in aliases: + with self.subTest(alias=alias): + safe_PlaySound(alias, winsound.SND_ALIAS) + + def test_alias_fallback(self): + safe_PlaySound('!"$%&/(#+*', winsound.SND_ALIAS) + + def test_alias_nofallback(self): + safe_PlaySound('!"$%&/(#+*', winsound.SND_ALIAS | winsound.SND_NODEFAULT) + + def test_stopasync(self): + safe_PlaySound( + 'SystemQuestion', + winsound.SND_ALIAS | winsound.SND_ASYNC | winsound.SND_LOOP + ) + time.sleep(0.5) + safe_PlaySound('SystemQuestion', winsound.SND_ALIAS | winsound.SND_NOSTOP) + # Issue 8367: PlaySound(None, winsound.SND_PURGE) + # does not raise on systems without a sound card. + winsound.PlaySound(None, winsound.SND_PURGE) + + def test_sound_sentry(self): + safe_PlaySound("SystemExit", winsound.SND_ALIAS | winsound.SND_SENTRY) + + def test_sound_sync(self): + safe_PlaySound("SystemExit", winsound.SND_ALIAS | winsound.SND_SYNC) + + def test_sound_system(self): + safe_PlaySound("SystemExit", winsound.SND_ALIAS | winsound.SND_SYSTEM) + + +if __name__ == "__main__": + unittest.main() diff --git a/crates/vm/Cargo.toml b/crates/vm/Cargo.toml index b89cda4fd..78b160867 100644 --- a/crates/vm/Cargo.toml +++ b/crates/vm/Cargo.toml @@ -119,6 +119,7 @@ workspace = true features = [ "Win32_Foundation", "Win32_Globalization", + "Win32_Media_Audio", "Win32_Networking_WinSock", "Win32_Security", "Win32_Security_Authorization", diff --git a/crates/vm/src/stdlib/mod.rs b/crates/vm/src/stdlib/mod.rs index 5bec43f72..e3bf42a4f 100644 --- a/crates/vm/src/stdlib/mod.rs +++ b/crates/vm/src/stdlib/mod.rs @@ -67,6 +67,8 @@ pub mod sys; mod winapi; #[cfg(all(feature = "host_env", windows))] mod winreg; +#[cfg(all(feature = "host_env", windows))] +mod winsound; use crate::{Context, builtins::PyModuleDef}; @@ -132,6 +134,8 @@ pub fn builtin_module_defs(ctx: &Context) -> Vec<&'static PyModuleDef> { #[cfg(all(feature = "host_env", windows))] winreg::module_def(ctx), #[cfg(all(feature = "host_env", windows))] + winsound::module_def(ctx), + #[cfg(all(feature = "host_env", windows))] _wmi::module_def(ctx), ] } diff --git a/crates/vm/src/stdlib/winsound.rs b/crates/vm/src/stdlib/winsound.rs new file mode 100644 index 000000000..3f65abbb8 --- /dev/null +++ b/crates/vm/src/stdlib/winsound.rs @@ -0,0 +1,206 @@ +// spell-checker:ignore pszSound fdwSound +#![allow(non_snake_case)] + +pub(crate) use winsound::module_def; + +mod win32 { + #[link(name = "winmm")] + unsafe extern "system" { + pub fn PlaySoundW(pszSound: *const u16, hmod: isize, fdwSound: u32) -> i32; + } + + unsafe extern "system" { + pub fn Beep(dwFreq: u32, dwDuration: u32) -> i32; + pub fn MessageBeep(uType: u32) -> i32; + } +} + +#[pymodule] +mod winsound { + use crate::builtins::{PyBytes, PyStr}; + use crate::common::windows::ToWideString; + use crate::convert::{IntoPyException, TryFromBorrowedObject}; + use crate::protocol::PyBuffer; + use crate::{AsObject, PyObjectRef, PyResult, VirtualMachine}; + + // PlaySound flags + #[pyattr] + const SND_SYNC: u32 = 0x0000; + #[pyattr] + const SND_ASYNC: u32 = 0x0001; + #[pyattr] + const SND_NODEFAULT: u32 = 0x0002; + #[pyattr] + const SND_MEMORY: u32 = 0x0004; + #[pyattr] + const SND_LOOP: u32 = 0x0008; + #[pyattr] + const SND_NOSTOP: u32 = 0x0010; + #[pyattr] + const SND_PURGE: u32 = 0x0040; + #[pyattr] + const SND_APPLICATION: u32 = 0x0080; + #[pyattr] + const SND_NOWAIT: u32 = 0x00002000; + #[pyattr] + const SND_ALIAS: u32 = 0x00010000; + #[pyattr] + const SND_FILENAME: u32 = 0x00020000; + #[pyattr] + const SND_SENTRY: u32 = 0x00080000; + #[pyattr] + const SND_SYSTEM: u32 = 0x00200000; + + // MessageBeep types + #[pyattr] + const MB_OK: u32 = 0x00000000; + #[pyattr] + const MB_ICONHAND: u32 = 0x00000010; + #[pyattr] + const MB_ICONQUESTION: u32 = 0x00000020; + #[pyattr] + const MB_ICONEXCLAMATION: u32 = 0x00000030; + #[pyattr] + const MB_ICONASTERISK: u32 = 0x00000040; + #[pyattr] + const MB_ICONERROR: u32 = MB_ICONHAND; + #[pyattr] + const MB_ICONSTOP: u32 = MB_ICONHAND; + #[pyattr] + const MB_ICONINFORMATION: u32 = MB_ICONASTERISK; + #[pyattr] + const MB_ICONWARNING: u32 = MB_ICONEXCLAMATION; + + #[derive(FromArgs)] + struct PlaySoundArgs { + #[pyarg(any)] + sound: PyObjectRef, + #[pyarg(any)] + flags: i32, + } + + #[pyfunction] + fn PlaySound(args: PlaySoundArgs, vm: &VirtualMachine) -> PyResult<()> { + let sound = args.sound; + let flags = args.flags as u32; + + if vm.is_none(&sound) { + let ok = unsafe { super::win32::PlaySoundW(core::ptr::null(), 0, flags) }; + if ok == 0 { + return Err(vm.new_runtime_error("Failed to play sound".to_owned())); + } + return Ok(()); + } + + if flags & SND_MEMORY != 0 { + if flags & SND_ASYNC != 0 { + return Err( + vm.new_runtime_error("Cannot play asynchronously from memory".to_owned()) + ); + } + let buffer = PyBuffer::try_from_borrowed_object(vm, &sound)?; + let buf = buffer.as_contiguous().ok_or_else(|| { + vm.new_type_error("a bytes-like object is required, not 'str'".to_owned()) + })?; + let ok = unsafe { super::win32::PlaySoundW(buf.as_ptr() as *const u16, 0, flags) }; + if ok == 0 { + return Err(vm.new_runtime_error("Failed to play sound".to_owned())); + } + return Ok(()); + } + + if sound.downcastable::() { + let type_name = sound.class().name().to_string(); + return Err(vm.new_type_error(format!( + "'sound' must be str, os.PathLike, or None, not {type_name}" + ))); + } + + // os.fspath(sound) + let path = match sound.downcast_ref::() { + Some(s) => s.as_str().to_owned(), + None => { + let fspath = vm.get_method_or_type_error( + sound.clone(), + identifier!(vm, __fspath__), + || { + let type_name = sound.class().name().to_string(); + format!("'sound' must be str, os.PathLike, or None, not {type_name}") + }, + )?; + + if vm.is_none(&fspath) { + return Err(vm.new_type_error(format!( + "'sound' must be str, os.PathLike, or None, not {}", + sound.class().name() + ))); + } + let result = fspath.call((), vm)?; + + if result.downcastable::() { + return Err( + vm.new_type_error("'sound' must resolve to str, not bytes".to_owned()) + ); + } + + let s: &PyStr = result.downcast_ref().ok_or_else(|| { + vm.new_type_error(format!( + "expected {}.__fspath__() to return str or bytes, not {}", + sound.class().name(), + result.class().name() + )) + })?; + + s.as_str().to_owned() + } + }; + + // Check for embedded null characters + if path.contains('\0') { + return Err(vm.new_value_error("embedded null character".to_owned())); + } + + let wide = path.to_wide_with_nul(); + let ok = unsafe { super::win32::PlaySoundW(wide.as_ptr(), 0, flags) }; + if ok == 0 { + return Err(vm.new_runtime_error("Failed to play sound".to_owned())); + } + Ok(()) + } + + #[derive(FromArgs)] + struct BeepArgs { + #[pyarg(any)] + frequency: i32, + #[pyarg(any)] + duration: i32, + } + + #[pyfunction] + fn Beep(args: BeepArgs, vm: &VirtualMachine) -> PyResult<()> { + if !(37..=32767).contains(&args.frequency) { + return Err(vm.new_value_error("frequency must be in 37 thru 32767".to_owned())); + } + + let ok = unsafe { super::win32::Beep(args.frequency as u32, args.duration as u32) }; + if ok == 0 { + return Err(vm.new_runtime_error("Failed to beep".to_owned())); + } + Ok(()) + } + + #[derive(FromArgs)] + struct MessageBeepArgs { + #[pyarg(any, default = 0)] + r#type: u32, + } + + #[pyfunction] + fn MessageBeep(args: MessageBeepArgs, vm: &VirtualMachine) -> PyResult<()> { + let ok = unsafe { super::win32::MessageBeep(args.r#type) }; + if ok == 0 { + return Err(std::io::Error::last_os_error().into_pyexception(vm)); + } + Ok(()) + } +}