alsactl 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 867a15dbe3727200b48ae90e35ad96c010bb19395b94f70629e2564979b2c76a
4
- data.tar.gz: b19233f1ee8a13d44fef4daaf58eab05077c50418299ff590966b8b35ae0aa18
3
+ metadata.gz: 482516915f981f7caead792ba342ce6c3cb641b581f30fcc1abb1aa4a738b345
4
+ data.tar.gz: d9901d56b145b182f74856571508c3a9c0c56373914ccd37e894f9ad5a3f63cf
5
5
  SHA512:
6
- metadata.gz: 6a5945c2b798ddee40b49fcff6fd35de7cc00063210464743eee7195022c4ac7c56c250df6fe3bc921f8cac9e381f005d856d956f53345cf44f379ab89d964fd
7
- data.tar.gz: b5832781e76557308f42a34253ad9978f5c90812cce6e96cb286793592c99d392f44ef407a43a780d93cbb308fc2fab776718529301ef78a2fad7d3b69264e53
6
+ metadata.gz: ebd6800c0e93d7bed7d50f6bf57735ac13929409a5e624e3001c99c3acaaa481c6604d1468a4603f8990691c27159ff2a1974707e313d6cde6993de97b4df423
7
+ data.tar.gz: 34070edd4b4bc9c6596cbb6eeaf269b7e5fc7a011f41f3d3bd987644fb5098f826d6920b4f6aee942a59f945ef63a46bc899295d206ffaf82bae5bcc093d2dc1
data/CHANGELOG.md CHANGED
@@ -1,4 +1,5 @@
1
1
  ### CHANGELOG -- (Ruby AlsaCtl) ###
2
2
  1) 0.1.0, unreleased: 2021-May-04 -- Changed the API to use Ruby as the complex implementation interface, because faster to get it done, less code, and way simpler than doing all the higher-order calculations (as well as string and symbol comparisons) in the C wrapper. [t. Edelweiss]
3
3
  1) 0.1.0, unreleased: 2021-May-04 -- In so doing above, turned entire C wrapper into an abstract class and module, so it can only be implemented by inheritance. Added such an implementation class, ``Mixers::DefMixer``. [t. Edelweiss]
4
- 1) 0.1.0, immediate pre-release: 2021-May-09 -- Realized that this gem was named too generic and changed the name to AlsaCtl, as there is a reasonable chance I might need to also handle PulseAudio or other APIs, at some point. Please also note the difference in the GitHub repo URL.
4
+ 1) 0.1.0, immediate pre-release: 2021-May-09 -- Realized that this gem was named too generic and changed the name to AlsaCtl, as there is a reasonable chance I might need to also handle PulseAudio or other APIs, at some point. Please also note the difference in the GitHub repo URL.
5
+ 1) 0.1.1, [CRITICAL PATCH] immediate pre-release: 2021-Jul-19 -- **CRITICAL PATCH (but does not change interface) -- please read** ``changes/0.1.1-critical.md`` **for information.** [t. Edelweiss]
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "rake/extensiontask"
2
+
3
+ # Build the AlsaCore extension
4
+ Rake::ExtensionTask.new "alsacore" do |ext|
5
+ ext.lib_dir = "ext/alsacore"
6
+ end # End build on AlsaCore
@@ -0,0 +1,20 @@
1
+ # SUMMARY #
2
+
3
+ **Date**
4
+ 2021-Jul-19
5
+
6
+ **Version**
7
+ 0.1.1
8
+
9
+ **Severity**
10
+ [CRITICAL UPDATE]
11
+
12
+ **Description**
13
+ No one appears to have noticed, perhaps due to the still highly-prevalent Ruby 2.7 on many systems, but this gem did NOT compile on later versions (2.8 BETA or anything 3.0+). This was due to a poorly-configured extconf, Rakefile, and gemspec. I have only been doing Ruby-C extensions for a few months, on and off, so I was not aware that Rake does not get invoked on execution of ``gem install``.
14
+
15
+ As such, the extension object file was not being placed into the correct location, even after a successful compilation, so the wrapper kept attempting to link _ONLY_ to the 2.7 version of the ``.so``. The key takeaway from this and why it would _still install_ was that the shared object, the one generated by my own local dev machine and older dev environment, was actually being placed into the proper location as a static dep, whereas the local, native, newly-compiled extension library was staying in an unlinked location (still under ``/ext/alsacore/``).
16
+
17
+ Thanks to a suggestion by @sampersand, on reviewing the ``openssl`` gem, I found out the correct way to link it was to leave the compiled object inside said directory, and to ``require 'alsacore.so'``, instead of using ``require_relative`` and the local path.
18
+
19
+ **Final Notes**
20
+ Please make sure to update this gem, if you have it installed, as the next Ruby version you upgrade to will NOT allow this to work, otherwise.
@@ -0,0 +1,372 @@
1
+ /* IN-PROCESS FILE - 2021-May-09 */
2
+ // Include Ruby
3
+ #include <ruby.h>
4
+
5
+ // Include other libraries
6
+ #include <stdio.h>
7
+ #include <math.h>
8
+
9
+ // ALSA includes
10
+ #include <alsa/asoundlib.h>
11
+ #include <alsa/version.h>
12
+
13
+ // Expansion to create a mixer pointer and then get the
14
+ // data from it
15
+ #define MIXLOADER base_mixer_obj* mixer; TypedData_Get_Struct(self, base_mixer_obj, &base_mixer_type, mixer);
16
+
17
+ // Don't try to handle anything on NULL mixer
18
+ #define CHK_MIX if (mixer->handle == NULL || \
19
+ mixer->element == NULL) { return Qnil; }
20
+
21
+ // Shorthand to allocate the channel arrays
22
+ #define MAX_CHANNELS (SND_MIXER_SCHN_LAST + 1)
23
+
24
+ // Shorthand early return on error
25
+ #define CRIT_CHECK if (err != 0) { return INT2NUM(err); }
26
+
27
+ /*
28
+ * Document-module: AlsaCore
29
+ *
30
+ * This module provides the concrete methods from which
31
+ * to extend custom mixers for volume control.
32
+ *
33
+ * A simple example of a default/Master mixer object is
34
+ * available and usable as +AlsaCtl::DefMixer+.
35
+ *
36
+ * = Example
37
+ *
38
+ * require 'AlsaCore'
39
+ *
40
+ * class MyMixer < AlsaCore::BaseMixer
41
+ */
42
+
43
+ // Basic type initiators used in this file
44
+ VALUE AlsaCore = Qnil;
45
+ VALUE BaseMixer = Qnil;
46
+ // End type initiators
47
+
48
+ /*
49
+ * Document-class: AlsaCore::BaseMixer
50
+ *
51
+ * The BaseMixer class provides purely protected methods
52
+ * that can be used from an inheriting class. +AlsaCtl::DefMixer+
53
+ * inherits this class and provides a simple interface without
54
+ * complex mixer management operations.
55
+ *
56
+ * BaseMixer provides methods for
57
+ * 1. Resource management
58
+ * 2. Connection
59
+ * 3. Disconnection
60
+ * 4. Setting ALL the volumes of a mixer
61
+ * 5. Per-channel volume setting of the mixer
62
+ * 6. Per-channel retrieval of mixer volume information
63
+ * 7. Enumeration of channels within the mixer
64
+ * 8. Key error handling for common operations
65
+ *
66
+ * The API can be found on https://www.alsa-project.org/alsa-doc/alsa-lib/
67
+ *
68
+ *---
69
+ * Document-method: pro_initialize
70
+ *
71
+ * (base_mixer_m_initialize)
72
+ *
73
+ * Protected method initializes the BaseMixer class.
74
+ *
75
+ * Instantiates the wrapped struct. Does not take any
76
+ * arguments.
77
+ *
78
+ * self.new -> BaseMixer
79
+ * self.initialize -> BaseMixer
80
+ *
81
+ *---
82
+ * Document-method: pro_connect
83
+ *
84
+ * (method_base_mixer_connect)
85
+ *
86
+ * Connects to a requested ALSA mixer.
87
+ *
88
+ * Takes 2 strings, a card name and element name (identifier).
89
+ * +DefMixer+ automatically passes the values "default" and "Master"
90
+ * to generate the default mixer element for the system.
91
+ *
92
+ * self.connect(char*, char*) -> BaseMixer | error
93
+ *
94
+ * pro_connect("default", "Master") -> new BaseMixer
95
+ *
96
+ *---
97
+ * Document-method: pro_close
98
+ *
99
+ * (method_base_mixer_disconnect)
100
+ *
101
+ * Disconnects the mixer and sets its struct member pointers
102
+ * to NULL.
103
+ *
104
+ * Takes no arguments.
105
+ *
106
+ * Returns true, if no error.
107
+ *
108
+ *---
109
+ * Document-method: pro_enum
110
+ *
111
+ * (method_base_mixer_enum_channels)
112
+ *
113
+ * Returns a hash of valid channels for the mixer, on success.
114
+ * On failure, returns +nil+.
115
+ *
116
+ * Takes no arguments.
117
+ *
118
+ *---
119
+ * Document-method: pro_cvolume_get
120
+ *
121
+ * (method_base_mixer_cvolume_get)
122
+ * Gets the volume settings for a selected channel on the mixer.
123
+ *
124
+ * Returns a hash with the volume, on success.
125
+ *
126
+ * On failure, returns either:
127
+ * +nil+, if the mixer is disconnected
128
+ * +false+, if the mixer doesn't have this channel
129
+ *
130
+ * Takes 1 argument, the channel number to check.
131
+ *
132
+ * pro_cvolume_get(1) -> {:name, :max, :min, :volume, :percent}
133
+ *
134
+ *---
135
+ * Document-method: pro_cvolume_set
136
+ *
137
+ * (method_base_mixer_cvolume_set)
138
+ *
139
+ * Sets the volume for a specific channel.
140
+ *
141
+ * Takes 2 arguments, the channel number (+int+) and the volume to use (+long+).
142
+ * When inheriting this class, you need to convert the values correctly,
143
+ * as 1 will not be 1%, but more like 0.1%, if your mixer uses 2^16 or something.
144
+ *
145
+ * Returns the output of method_base_mixer_cvolume_get for the same channel,
146
+ * on success.
147
+ *
148
+ * Returns +nil+, on failure.
149
+ *
150
+ * pro_cvolume_set(1, 32768) -> Sets channel 1 to 32768 (usually 50%)
151
+ *
152
+ *---
153
+ * Document-method: pro_volume_set
154
+ *
155
+ * (method_base_mixer_volume_set_all)
156
+ *
157
+ * Sets the volumes for the entire mixer, all channels. This method
158
+ * uses a separate call from ALSA, +snd_mixer_selem_set_playback_volume_all+,
159
+ * which does not require any channels.
160
+ *
161
+ * Takes 1 argument, the volume (+long+).
162
+ *
163
+ * pro_volume_set(32768) -> Sets all channels to 32768 (probably 50%)
164
+ */
165
+
166
+ // ---------------------------
167
+ // MIXER STRUCT BELOW
168
+ // ---------------------------
169
+
170
+ // Define the base mixer struct
171
+ typedef struct {
172
+ snd_mixer_t* handle; // Mixer handle
173
+ snd_mixer_elem_t* element; // Mixer element
174
+ } base_mixer_obj; // End Mixer struct def
175
+
176
+ // ---------------------------
177
+ // MIXER METHODS BELOW
178
+ // ---------------------------
179
+
180
+ // Free method for base mixer
181
+ void base_mixer_free(void* data) {
182
+ free(data);
183
+ } // End mixer free
184
+
185
+ // Size of mixer
186
+ size_t base_mixer_size(const void* data) {
187
+ return sizeof(data);
188
+ } // End size of mixer
189
+
190
+ // Define the Ruby struct type to encapsulate the
191
+ // master sound mixer
192
+ static const rb_data_type_t base_mixer_type = {
193
+ .wrap_struct_name = "base_mixer_obj",
194
+ .function = {
195
+ .dmark = NULL,
196
+ .dfree = base_mixer_free,
197
+ .dsize = base_mixer_size,
198
+ },
199
+ .data = NULL,
200
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
201
+ }; // End base mixer type definition
202
+
203
+ // Define allocation of mixer handle object
204
+ VALUE base_mixer_alloc(VALUE self) {
205
+ // Allocate the size for a mixer handle and its opened element
206
+ base_mixer_obj* mixer = malloc(sizeof(base_mixer_obj));
207
+ return TypedData_Wrap_Struct(self, &base_mixer_type, mixer); // Wrap it up!
208
+ } // End mixer handle allocation
209
+
210
+ // Initialize / constructor
211
+ VALUE base_mixer_m_initialize(VALUE self) {
212
+ // The below are set on connect() and voided on disconnect()
213
+ snd_mixer_t *handle = NULL; // NULL handle
214
+ snd_mixer_elem_t* elem = NULL; // NULL element
215
+
216
+ // Create a Base Mixer struct pointer to access
217
+ // from the class
218
+ base_mixer_obj mm = {
219
+ .handle = handle,
220
+ .element = elem,
221
+ };
222
+
223
+ MIXLOADER; // Shorthand load the mixer
224
+ *mixer = mm; // Bind the struct to the class object's pointer
225
+ return self;
226
+ } // End mix handle initializer
227
+
228
+ // Connect with the ALSA server
229
+ VALUE method_base_mixer_connect(VALUE self, VALUE card_name, VALUE elem_name) {
230
+ int err = 0; // Placeholder for errors
231
+ snd_mixer_selem_id_t *sid; // Create mixer element ID
232
+
233
+ // Default mixer will be default/Master
234
+ const char *card = StringValueCStr(card_name); // ex: "default"
235
+ const char *selem_name = StringValueCStr(elem_name); // ex: "Master"
236
+
237
+ MIXLOADER; // Shorthand load the mixer
238
+ // If the mixer is already connected, raise an error
239
+ if (mixer->handle != NULL || mixer->element != NULL) {
240
+ rb_raise(rb_eRuntimeError,
241
+ "\n::ERROR: Already connected to mixer! Disconnect first!::\n");
242
+ return Qnil;
243
+ }
244
+
245
+ // The below will use CRIT_CHECK, as these errors are
246
+ // non-recoverable, and continuation in other methods
247
+ // will result in seg faults.
248
+ //
249
+ // Open and load a mixer object
250
+ err = snd_mixer_open(&mixer->handle, 0);
251
+ CRIT_CHECK;
252
+ err = snd_mixer_attach(mixer->handle, card);
253
+ CRIT_CHECK;
254
+ err = snd_mixer_selem_register(mixer->handle, NULL, NULL);
255
+ CRIT_CHECK;
256
+ err = snd_mixer_load(mixer->handle);
257
+ CRIT_CHECK;
258
+
259
+ // Attach a mixer element to the handle
260
+ snd_mixer_selem_id_alloca(&sid);
261
+ snd_mixer_selem_id_set_index(sid, 0);
262
+ snd_mixer_selem_id_set_name(sid, selem_name);
263
+
264
+ // Instantiate the mixer element attached to the handle
265
+ mixer->element = snd_mixer_find_selem(mixer->handle, sid);
266
+
267
+ // Checker for valid channels moved to Ruby side
268
+ return self;
269
+ } // End connect method
270
+
271
+ // Disconnect from the ALSA server
272
+ VALUE method_base_mixer_disconnect(VALUE self) {
273
+ MIXLOADER; // Shorthand load the mixer
274
+ if (mixer->handle != NULL) {
275
+ int err = snd_mixer_close(mixer->handle); // Close the mixer
276
+ CRIT_CHECK;
277
+ mixer->element = NULL;
278
+ mixer->handle = NULL;
279
+ } // If it's already NULL, there is nothing to close,
280
+ // So returning TRUE is still saying the mixer is disconnected
281
+ return Qtrue;
282
+ } // End disconnect method
283
+
284
+ // Enumerate the channels
285
+ VALUE method_base_mixer_enum_channels(VALUE self) {
286
+ MIXLOADER; // Shorthand load the mixer
287
+ CHK_MIX; // Check if mixer is null or not, before attempting operations
288
+ VALUE channels = rb_hash_new(); // Hash of channels
289
+ for (int i = 0; i < (MAX_CHANNELS); i++) {
290
+ // Check if the channel exists; if so, give it a true key.
291
+ // Ignore any channels not present -- a bad index returns NIL, anyway
292
+ if (snd_mixer_selem_has_playback_channel(mixer->element, i)) {
293
+ rb_hash_aset(channels, INT2NUM(i), Qtrue);
294
+ }
295
+ }
296
+ return channels;
297
+ } // End channel enumerator
298
+
299
+ // Get the volume for a single channel
300
+ VALUE method_base_mixer_cvolume_get(VALUE self, VALUE cid) {
301
+ long min, max, current; // Volume values
302
+ MIXLOADER; // Shorthand load the mixer
303
+ CHK_MIX; // Check if mixer is null or not, before attempting operations
304
+ snd_mixer_handle_events(mixer->handle); // Refresh events on handle
305
+ snd_mixer_selem_get_playback_volume_range(mixer->element, &min, &max); // Get volume range
306
+
307
+ // Get the volume for the channel, but only if cid (channel id) is valid
308
+ if (RB_TYPE_P(cid, T_FIXNUM)){
309
+ int cid_fix = NUM2INT(cid);
310
+ if (snd_mixer_selem_has_playback_channel(mixer->element, cid_fix)) {
311
+ snd_mixer_selem_get_playback_volume(mixer->element, cid_fix, &current);
312
+ VALUE channel = rb_hash_new(); // Channel has volume and percent
313
+ // Name of channel, if available
314
+ rb_hash_aset(channel, rb_id2sym(rb_intern("name")),
315
+ rb_str_new2(snd_mixer_selem_channel_name(cid_fix)));
316
+ // Minimum and maximum values
317
+ rb_hash_aset(channel, rb_id2sym(rb_intern("max")), LONG2NUM(max));
318
+ rb_hash_aset(channel, rb_id2sym(rb_intern("min")), LONG2NUM(min));
319
+ // Current channel volume
320
+ rb_hash_aset(channel, rb_id2sym(rb_intern("volume")), LONG2NUM(current));
321
+ // Get the percentage of the total (round to 0 decimals,
322
+ // for reasons logical to audio use)
323
+ int perc = (int) round(( ((double) current / (double) max) ) * 100);
324
+ rb_hash_aset(channel, rb_id2sym(rb_intern("percent")), INT2NUM(perc));
325
+ return channel;
326
+ } else { return Qfalse; } // False if channel doesn't exist
327
+ } else { return Qnil; } // Nil if channel id is not a number
328
+ } // End getter for single-channel volume
329
+
330
+ // Set volume on a single channel
331
+ VALUE method_base_mixer_cvolume_set(VALUE self, VALUE channel, VALUE volume) {
332
+ MIXLOADER; // Shorthand load the mixer
333
+ CHK_MIX; // Check if mixer is null or not, before attempting operations
334
+ int ch = NUM2INT(channel); // Cast the channel to an INT
335
+ // Set the volume for one channel
336
+ snd_mixer_selem_set_playback_volume(mixer->element, ch, NUM2LONG(volume));
337
+ return method_base_mixer_cvolume_get(self, channel);
338
+ } // End set channel volume to specified
339
+
340
+ // Set ALL the channels' volumes on the mixer
341
+ VALUE method_base_mixer_volume_set_all(VALUE self, VALUE volume) {
342
+ MIXLOADER; // Shorthand load the mixer
343
+ CHK_MIX; // Check if mixer is null or not, before attempting operations
344
+ // Set the volume for all channels
345
+ int err = snd_mixer_selem_set_playback_volume_all(mixer->element, NUM2LONG(volume));
346
+ return (err ? Qfalse : Qtrue);
347
+ } // End set volume to specified
348
+
349
+ // Initialize the AlsaCore module
350
+ void Init_alsacore() {
351
+ AlsaCore = rb_define_module("AlsaCore"); // Module
352
+
353
+ // Mixer class
354
+ BaseMixer = rb_define_class_under(AlsaCore, "BaseMixer", rb_cData);
355
+ rb_define_alloc_func(BaseMixer, base_mixer_alloc);
356
+ rb_define_protected_method(BaseMixer, "pro_initialize", base_mixer_m_initialize, 0);
357
+
358
+ // Connection methods
359
+ rb_define_protected_method(BaseMixer, "pro_connect", method_base_mixer_connect, 2);
360
+ rb_define_protected_method(BaseMixer, "pro_disconnect", method_base_mixer_disconnect, 0);
361
+ rb_define_protected_method(BaseMixer, "pro_close", method_base_mixer_disconnect, 0);
362
+
363
+ // Enumerate the channels
364
+ rb_define_protected_method(BaseMixer, "pro_enum", method_base_mixer_enum_channels, 0);
365
+
366
+ // Get a channel's volume
367
+ rb_define_protected_method(BaseMixer, "pro_cvolume_get", method_base_mixer_cvolume_get, 1);
368
+
369
+ // Setters
370
+ rb_define_protected_method(BaseMixer, "pro_volume_set", method_base_mixer_volume_set_all, 1);
371
+ rb_define_protected_method(BaseMixer, "pro_cvolume_set", method_base_mixer_cvolume_set, 2);
372
+ } // End init
@@ -1,10 +1,10 @@
1
1
  # ext/alsacore/extconf.rb
2
2
  require 'mkmf'
3
3
 
4
- # Build MixCore
4
+ # Build AlsaCore
5
5
  $LFLAGS = '-lasound'
6
6
  have_library("asound")
7
7
  have_header("alsa/asoundlib.h")
8
8
  have_func("snd_mixer_open")
9
9
  have_func("snd_mixer_close")
10
- create_makefile("alsacore/alsacore")
10
+ create_makefile("alsacore")
@@ -1,6 +1,6 @@
1
1
  # lib/alsactl/mixer.rb
2
2
 
3
- require_relative "../alsacore/alsacore"
3
+ require "alsacore.so"
4
4
 
5
5
  ##
6
6
  # This extends the regular AlsaCore module and
@@ -6,5 +6,5 @@ module AlsaCtl
6
6
  ##
7
7
  # Current version -- I'm only documenting this, so
8
8
  # +rdoc+ doesn't complain.
9
- VERSION = "0.1.0"
9
+ VERSION = "0.1.1"
10
10
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alsactl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Edelweiss
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-09 00:00:00.000000000 Z
11
+ date: 2021-07-20 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  ALSA wrapper in Ruby, utilizing a purely-abstract C-Ruby API backend
@@ -21,12 +21,15 @@ extra_rdoc_files:
21
21
  - README.md
22
22
  - CHANGELOG.md
23
23
  - LICENSE.txt
24
+ - changes/0.1.1-critical.md
24
25
  files:
25
26
  - CHANGELOG.md
26
27
  - LICENSE.txt
27
28
  - README.md
29
+ - Rakefile
30
+ - changes/0.1.1-critical.md
31
+ - ext/alsacore/alsacore.c
28
32
  - ext/alsacore/extconf.rb
29
- - lib/alsacore/alsacore.so
30
33
  - lib/alsactl.rb
31
34
  - lib/alsactl/mixers.rb
32
35
  - lib/alsactl/version.rb
Binary file